Skip to content
1 change: 1 addition & 0 deletions BTCPayServer.Client/Models/InvoiceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public class CheckoutOptions
public string DefaultLanguage { get; set; }
public CheckoutType? CheckoutType { get; set; }
public bool? LazyPaymentMethods { get; set; }
public string ExplicitRateScript { get; set; }
}
}
public class InvoiceData : InvoiceDataBase
Expand Down
68 changes: 43 additions & 25 deletions BTCPayServer.Tests/SeleniumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2361,11 +2361,11 @@ public async Task CanUseLNAddress()
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();

Expand All @@ -2375,20 +2375,22 @@ public async Task CanUseLNAddress()
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)
{
Expand All @@ -2397,41 +2399,38 @@ public async Task CanUseLNAddress()
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):
Assert.StartsWith(lnaddress1 + "@", m["text/identifier"]);
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}"))
{
Expand Down Expand Up @@ -2484,6 +2483,25 @@ public async Task CanUseLNAddress()
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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ public class GreenfieldInvoiceController : Controller
private readonly RateFetcher _rateProvider;
private readonly InvoiceActivator _invoiceActivator;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IAuthorizationService _authorizationService;

public LanguageService LanguageService { get; }

public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
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;
Expand All @@ -59,6 +60,7 @@ public GreenfieldInvoiceController(UIInvoiceController invoiceController, Invoic
_invoiceActivator = invoiceActivator;
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
_authorizationService = authorizationService;
LanguageService = languageService;
}

Expand Down Expand Up @@ -188,6 +190,12 @@ public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequ
{
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)
{
Expand Down
7 changes: 5 additions & 2 deletions BTCPayServer/Controllers/UIInvoiceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoi
{
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);
Expand Down Expand Up @@ -315,7 +319,6 @@ internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(InvoiceEntity entity, St
}
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)
{
Expand All @@ -337,7 +340,7 @@ internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(InvoiceEntity entity, St
}
}

var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var rateRules = entity.ExplicitRateRules?? storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);

Expand Down
Loading