mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Allow LN Address to customize invoice metadata, and various bug fixes on LNUrl (#4855)
* Allow LN Address to customize invoice metadata solves https://github.com/OpenSats/website/issues/8 * Refactor GetLNUrl * Fix lightningAddresssettings.Max being ignored * Fix: The payRequest generated by the callback wasn't the same as the original --------- Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Data;
|
namespace BTCPayServer.Data;
|
||||||
|
|
||||||
@@ -38,4 +39,6 @@ public class LightningAddressDataBlob
|
|||||||
public string CurrencyCode { get; set; }
|
public string CurrencyCode { get; set; }
|
||||||
public decimal? Min { get; set; }
|
public decimal? Min { get; set; }
|
||||||
public decimal? Max { get; set; }
|
public decimal? Max { get; set; }
|
||||||
|
|
||||||
|
public JObject InvoiceMetadata { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reflection.Metadata;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -17,7 +16,6 @@ using BTCPayServer.Lightning;
|
|||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Views.Manage;
|
using BTCPayServer.Views.Manage;
|
||||||
using BTCPayServer.Views.Server;
|
using BTCPayServer.Views.Server;
|
||||||
@@ -29,6 +27,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using OpenQA.Selenium.Support.Extensions;
|
using OpenQA.Selenium.Support.Extensions;
|
||||||
@@ -2256,13 +2255,16 @@ namespace BTCPayServer.Tests
|
|||||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
|
||||||
s.Driver.ToggleCollapse("AddAddress");
|
s.Driver.ToggleCollapse("AddAddress");
|
||||||
|
|
||||||
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
|
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
|
||||||
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);
|
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);
|
||||||
|
lnaddress2 = lnaddress2.ToLowerInvariant();
|
||||||
|
|
||||||
s.Driver.ToggleCollapse("AdvancedSettings");
|
s.Driver.ToggleCollapse("AdvancedSettings");
|
||||||
s.Driver.FindElement(By.Id("Add_CurrencyCode")).SendKeys("EUR");
|
s.Driver.FindElement(By.Id("Add_CurrencyCode")).SendKeys("EUR");
|
||||||
s.Driver.FindElement(By.Id("Add_Min")).SendKeys("2");
|
s.Driver.FindElement(By.Id("Add_Min")).SendKeys("2");
|
||||||
s.Driver.FindElement(By.Id("Add_Max")).SendKeys("10");
|
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.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
|
||||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
|
||||||
@@ -2278,20 +2280,100 @@ namespace BTCPayServer.Tests
|
|||||||
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString()
|
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString()
|
||||||
.Replace("https", "http"));
|
.Replace("https", "http"));
|
||||||
var request = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
|
var request = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
|
||||||
|
var m = request.ParsedMetadata.ToDictionary(o => o.Key, o => o.Value);
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
case { } v when v.StartsWith(lnaddress2):
|
case { } v when v.StartsWith(lnaddress2):
|
||||||
|
Assert.StartsWith(lnaddress2 + "@", m["text/identifier"]);
|
||||||
|
lnaddress2 = m["text/identifier"];
|
||||||
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||||
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case { } v when v.StartsWith(lnaddress1):
|
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(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||||
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
|
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
Assert.False(true, "Should have matched");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 lnUsername = lnaddress1.Split('@')[0];
|
||||||
|
LNURLPayRequest req;
|
||||||
|
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
|
||||||
|
{
|
||||||
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
req = JsonConvert.DeserializeObject<LNURLPayRequest>(str);
|
||||||
|
Assert.Contains(req.ParsedMetadata, m => m.Key == "text/identifier" && m.Value == lnaddress1);
|
||||||
|
Assert.Contains(req.ParsedMetadata, m => m.Key == "text/plain" && m.Value.StartsWith("Paid to"));
|
||||||
|
Assert.NotNull(req.Callback);
|
||||||
|
Assert.Equal(new LightMoney(1000), req.MinSendable);
|
||||||
|
Assert.Equal(LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC), req.MaxSendable);
|
||||||
|
}
|
||||||
|
lnUsername = lnaddress2.Split('@')[0];
|
||||||
|
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
|
||||||
|
{
|
||||||
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
req = JsonConvert.DeserializeObject<LNURLPayRequest>(str);
|
||||||
|
Assert.Equal(new LightMoney(2000), req.MinSendable);
|
||||||
|
Assert.Equal(new LightMoney(10_000), req.MaxSendable);
|
||||||
|
}
|
||||||
|
// Check if we can get the same payrequest through the callback
|
||||||
|
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback))
|
||||||
|
{
|
||||||
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
req = JsonConvert.DeserializeObject<LNURLPayRequest>(str);
|
||||||
|
Assert.Equal(new LightMoney(2000), req.MinSendable);
|
||||||
|
Assert.Equal(new LightMoney(10_000), req.MaxSendable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can we ask for invoice? (Should fail, below minSpendable)
|
||||||
|
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=1999"))
|
||||||
|
{
|
||||||
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
var err = JsonConvert.DeserializeObject<LNUrlStatusResponse>(str);
|
||||||
|
Assert.Equal("Amount is out of bounds.", err.Reason);
|
||||||
|
}
|
||||||
|
// Can we ask for invoice?
|
||||||
|
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2000"))
|
||||||
|
{
|
||||||
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
|
||||||
|
Assert.NotNull(succ.Pr);
|
||||||
|
Assert.Equal(new LightMoney(2000), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can we change comment?
|
||||||
|
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2001"))
|
||||||
|
{
|
||||||
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
|
||||||
|
Assert.NotNull(succ.Pr);
|
||||||
|
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
||||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||||
|
|
||||||
@@ -244,17 +245,6 @@ namespace BTCPayServer
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
|
||||||
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
|
||||||
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
|
||||||
var lnUrlMethod =
|
|
||||||
methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod;
|
|
||||||
var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi);
|
|
||||||
if (lnUrlMethod is null || lnMethod is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewPointOfSaleViewModel.Item[] items;
|
ViewPointOfSaleViewModel.Item[] items;
|
||||||
string currencyCode;
|
string currencyCode;
|
||||||
PointOfSaleSettings posS = null;
|
PointOfSaleSettings posS = null;
|
||||||
@@ -278,6 +268,9 @@ namespace BTCPayServer
|
|||||||
ViewPointOfSaleViewModel.Item item = null;
|
ViewPointOfSaleViewModel.Item item = null;
|
||||||
if (!string.IsNullOrEmpty(itemCode))
|
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);
|
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
||||||
item = items.FirstOrDefault(item1 =>
|
item = items.FirstOrDefault(item1 =>
|
||||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
@@ -296,8 +289,38 @@ namespace BTCPayServer
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
|
var createInvoice = new CreateInvoiceRequest()
|
||||||
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item?.Price.Value, true));
|
{
|
||||||
|
Amount = item?.Price.Value,
|
||||||
|
Currency = currencyCode,
|
||||||
|
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||||
|
{
|
||||||
|
RedirectURL = app.AppType switch
|
||||||
|
{
|
||||||
|
PointOfSaleAppType.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
|
||||||
|
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
|
||||||
|
_ => null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var invoiceMetadata = new InvoiceMetadata();
|
||||||
|
invoiceMetadata.OrderId =AppService.GetAppOrderId(app);
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
invoiceMetadata.ItemCode = item.Id;
|
||||||
|
invoiceMetadata.ItemDesc = item.Description;
|
||||||
|
}
|
||||||
|
createInvoice.Metadata = invoiceMetadata.ToJObject();
|
||||||
|
|
||||||
|
|
||||||
|
return await GetLNURLRequest(
|
||||||
|
cryptoCode,
|
||||||
|
store,
|
||||||
|
store.GetStoreBlob(),
|
||||||
|
createInvoice,
|
||||||
|
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
|
||||||
|
allowOverpay: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EditLightningAddressVM
|
public class EditLightningAddressVM
|
||||||
@@ -327,6 +350,9 @@ namespace BTCPayServer
|
|||||||
[Display(Name = "Max sats")]
|
[Display(Name = "Max sats")]
|
||||||
[Range(1, double.PositiveInfinity)]
|
[Range(1, double.PositiveInfinity)]
|
||||||
public decimal? Max { get; set; }
|
public decimal? Max { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Invoice metadata")]
|
||||||
|
public string InvoiceMetadata { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new ();
|
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new ();
|
||||||
@@ -344,111 +370,103 @@ namespace BTCPayServer
|
|||||||
public async Task<IActionResult> ResolveLightningAddress(string username)
|
public async Task<IActionResult> ResolveLightningAddress(string username)
|
||||||
{
|
{
|
||||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||||
if (lightningAddressSettings is null)
|
if (lightningAddressSettings is null || username is null)
|
||||||
{
|
return NotFound("Unknown username");
|
||||||
|
|
||||||
|
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||||
|
if (store is null)
|
||||||
return NotFound("Unknown username");
|
return NotFound("Unknown username");
|
||||||
}
|
|
||||||
|
|
||||||
var blob = lightningAddressSettings.GetBlob();
|
var blob = lightningAddressSettings.GetBlob();
|
||||||
return await GetLNURL("BTC", lightningAddressSettings.StoreDataId, blob.CurrencyCode, blob.Min, blob.Max,
|
|
||||||
() => (username, null, null, null, null, true));
|
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}" }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("pay")]
|
[HttpGet("pay")]
|
||||||
public async Task<IActionResult> GetLNURL(string cryptoCode, string storeId, string currencyCode = null,
|
[EnableCors(CorsPolicies.All)]
|
||||||
decimal? min = null, decimal? max = null,
|
[IgnoreAntiforgeryToken]
|
||||||
Func<(string username, AppData app, ViewPointOfSaleViewModel.Item item, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice)>
|
public async Task<IActionResult> GetLNUrlForStore(
|
||||||
internalDetails = null)
|
string cryptoCode,
|
||||||
|
string storeId,
|
||||||
|
string currencyCode = null)
|
||||||
{
|
{
|
||||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
var store = this.HttpContext.GetStoreData();
|
||||||
if (network is null || !network.SupportLightning)
|
|
||||||
{
|
|
||||||
return NotFound("This network does not support Lightning");
|
|
||||||
}
|
|
||||||
|
|
||||||
var store = await _storeRepository.FindStore(storeId);
|
|
||||||
if (store is null)
|
if (store is null)
|
||||||
{
|
return NotFound();
|
||||||
return NotFound("Store not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
var storeBlob = store.GetStoreBlob();
|
|
||||||
currencyCode ??= storeBlob.DefaultCurrency ?? cryptoCode;
|
|
||||||
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
|
||||||
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
|
||||||
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
|
||||||
var lnUrlMethod =
|
|
||||||
methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod;
|
|
||||||
var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi);
|
|
||||||
if (lnUrlMethod is null || lnMethod is null)
|
|
||||||
{
|
|
||||||
return NotFound("LNURL or Lightning payment method not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
var blob = store.GetStoreBlob();
|
var blob = store.GetStoreBlob();
|
||||||
if (blob.GetExcludedPaymentMethods().Match(pmi) || blob.GetExcludedPaymentMethods().Match(lnpmi))
|
if (!blob.AnyoneCanInvoice)
|
||||||
|
return NotFound("'Anyone can invoice' is turned off");
|
||||||
|
return await GetLNURLRequest(
|
||||||
|
cryptoCode,
|
||||||
|
store,
|
||||||
|
blob,
|
||||||
|
new CreateInvoiceRequest
|
||||||
{
|
{
|
||||||
return NotFound("LNURL or Lightning payment method disabled");
|
Currency = currencyCode
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
(string username, AppData app, ViewPointOfSaleViewModel.Item item, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice) =
|
private async Task<IActionResult> GetLNURLRequest(
|
||||||
(internalDetails ?? (() => (null, null, null, null, null, null)))();
|
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)
|
||||||
|
{
|
||||||
|
if (GetLNUrlPaymentMethodId(cryptoCode, store, out _) is null)
|
||||||
|
return NotFound("LNUrl or LN is disabled");
|
||||||
|
|
||||||
if ((anyoneCanInvoice ?? blob.AnyoneCanInvoice) is false)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var lnAddress = username is null ? null : $"{username}@{Request.Host}";
|
|
||||||
List<string[]> lnurlMetadata = new();
|
|
||||||
|
|
||||||
var redirectUrl = app?.AppType switch
|
|
||||||
{
|
|
||||||
PointOfSaleAppType.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
|
|
||||||
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
var invoiceRequest = new CreateInvoiceRequest
|
|
||||||
{
|
|
||||||
Amount = invoiceAmount,
|
|
||||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
|
||||||
{
|
|
||||||
PaymentMethods = new[] { pmi.ToStringNormalized() },
|
|
||||||
Expiration = blob.InvoiceExpiration < TimeSpan.FromMinutes(2)
|
|
||||||
? blob.InvoiceExpiration
|
|
||||||
: TimeSpan.FromMinutes(2),
|
|
||||||
RedirectURL = redirectUrl
|
|
||||||
},
|
|
||||||
Currency = currencyCode,
|
|
||||||
Type = invoiceAmount is null ? InvoiceType.TopUp : InvoiceType.Standard,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
invoiceRequest.Metadata =
|
|
||||||
new InvoiceMetadata
|
|
||||||
{
|
|
||||||
ItemCode = item.Id,
|
|
||||||
ItemDesc = item.Description,
|
|
||||||
OrderId = AppService.GetAppOrderId(app)
|
|
||||||
}.ToJObject();
|
|
||||||
}
|
|
||||||
InvoiceEntity i;
|
InvoiceEntity i;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
i = await _invoiceController.CreateInvoiceCoreRaw(invoiceRequest, store, Request.GetAbsoluteRoot(), additionalTags);
|
i = await _invoiceController.CreateInvoiceCoreRaw(createInvoice, store, Request.GetAbsoluteRoot(), additionalTags);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return this.CreateAPIError(null, e.Message);
|
return this.CreateAPIError(null, e.Message);
|
||||||
}
|
}
|
||||||
if (i.Type != InvoiceType.TopUp)
|
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
|
||||||
{
|
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
|
||||||
min = i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi);
|
|
||||||
max = item?.Price?.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum ? null : min;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(username))
|
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(
|
||||||
|
string cryptoCode,
|
||||||
|
InvoiceEntity i,
|
||||||
|
Data.StoreData store,
|
||||||
|
StoreBlob blob,
|
||||||
|
LNURLPayRequest lnurlRequest = null,
|
||||||
|
Dictionary<string, string> lnUrlMetadata = null,
|
||||||
|
bool allowOverpay = true)
|
||||||
|
{
|
||||||
|
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod);
|
||||||
|
if (pmi is null)
|
||||||
|
return null;
|
||||||
|
lnurlRequest ??= new LNURLPayRequest();
|
||||||
|
lnUrlMetadata ??= new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (lnUrlMetadata?.TryGetValue("text/identifier", out var lnAddress) is true && lnAddress is string)
|
||||||
{
|
{
|
||||||
var pm = i.GetPaymentMethod(pmi);
|
var pm = i.GetPaymentMethod(pmi);
|
||||||
var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails();
|
var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails();
|
||||||
@@ -457,36 +475,68 @@ namespace BTCPayServer
|
|||||||
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
|
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!lnUrlMetadata.ContainsKey("text/plain"))
|
||||||
|
{
|
||||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||||
lnurlMetadata.Add(new[] { "text/plain", invoiceDescription });
|
|
||||||
if (!string.IsNullOrEmpty(username))
|
|
||||||
{
|
|
||||||
lnurlMetadata.Add(new[] { "text/identifier", lnAddress });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await _pluginHookService.ApplyFilter("modify-lnurlp-request", new LNURLPayRequest
|
lnurlRequest.Tag = "payRequest";
|
||||||
{
|
lnurlRequest.CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0;
|
||||||
Tag = "payRequest",
|
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||||
MinSendable = new LightMoney(min ?? 1m, LightMoneyUnit.Satoshi),
|
|
||||||
MaxSendable =
|
|
||||||
max is null
|
|
||||||
? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC)
|
|
||||||
: new LightMoney(max.Value, LightMoneyUnit.Satoshi),
|
|
||||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0,
|
|
||||||
Metadata = JsonConvert.SerializeObject(lnurlMetadata),
|
|
||||||
Callback = new Uri(_linkGenerator.GetUriByAction(
|
|
||||||
action: nameof(GetLNURLForInvoice),
|
action: nameof(GetLNURLForInvoice),
|
||||||
controller: "UILNURL",
|
controller: "UILNURL",
|
||||||
values: new {cryptoCode, invoiceId = i.Id}, Request.Scheme, Request.Host, Request.PathBase))
|
values: new { pmi.CryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase));
|
||||||
}) is not LNURLPayRequest lnurlp)
|
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||||
|
if (i.Type != InvoiceType.TopUp)
|
||||||
{
|
{
|
||||||
return NotFound();
|
lnurlRequest.MinSendable = new LightMoney(i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
|
||||||
|
if (!allowOverpay)
|
||||||
|
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
|
||||||
}
|
}
|
||||||
return Ok(lnurlp);
|
|
||||||
|
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||||
|
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||||
|
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||||
|
|
||||||
|
if (lnurlRequest.MaxSendable is null)
|
||||||
|
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||||
|
|
||||||
|
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||||
|
|
||||||
|
i.Metadata ??= new InvoiceMetadata();
|
||||||
|
var metadata = i.Metadata.ToJObject();
|
||||||
|
if (metadata.Property("payRequest") is null)
|
||||||
|
{
|
||||||
|
metadata.Add("payRequest", JToken.FromObject(lnurlRequest));
|
||||||
|
await _invoiceRepository.UpdateInvoiceMetadata(i.Id, i.StoreId, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnurlRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
|
||||||
|
{
|
||||||
|
lnUrlSettings = null;
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
|
if (network is null || !network.SupportLightning)
|
||||||
|
return null;
|
||||||
|
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
||||||
|
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||||
|
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
||||||
|
var lnUrlMethod =
|
||||||
|
methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod;
|
||||||
|
var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi);
|
||||||
|
if (lnUrlMethod is null || lnMethod is null)
|
||||||
|
return null;
|
||||||
|
var blob = store.GetStoreBlob();
|
||||||
|
if (blob.GetExcludedPaymentMethods().Match(pmi) || blob.GetExcludedPaymentMethods().Match(lnpmi))
|
||||||
|
return null;
|
||||||
|
lnUrlSettings = lnUrlMethod;
|
||||||
|
return pmi;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("pay/i/{invoiceId}")]
|
[HttpGet("pay/i/{invoiceId}")]
|
||||||
@@ -501,60 +551,45 @@ namespace BTCPayServer
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment is not null)
|
|
||||||
comment = comment.Truncate(2000);
|
|
||||||
|
|
||||||
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
|
||||||
var i = await _invoiceRepository.GetInvoice(invoiceId, true);
|
var i = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||||
|
if (i is null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
var store = await _storeRepository.FindStore(i.StoreId);
|
var store = await _storeRepository.FindStore(i.StoreId);
|
||||||
if (store is null)
|
if (store is null)
|
||||||
{
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
|
|
||||||
if (i.Status == InvoiceStatusLegacy.New)
|
if (i.Status == InvoiceStatusLegacy.New)
|
||||||
{
|
{
|
||||||
var isTopup = i.IsUnsetTopUp();
|
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out var lnurlSupportedPaymentMethod);
|
||||||
var lnurlSupportedPaymentMethod =
|
if (pmi is null)
|
||||||
i.GetSupportedPaymentMethod<LNURLPaySupportedPaymentMethod>(pmi).FirstOrDefault();
|
|
||||||
if (lnurlSupportedPaymentMethod is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
|
|
||||||
var lightningPaymentMethod = i.GetPaymentMethod(pmi);
|
var lightningPaymentMethod = i.GetPaymentMethod(pmi);
|
||||||
var accounting = lightningPaymentMethod.Calculate();
|
|
||||||
var paymentMethodDetails =
|
var paymentMethodDetails =
|
||||||
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
|
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
|
||||||
if (paymentMethodDetails.LightningSupportedPaymentMethod is null)
|
if (paymentMethodDetails.LightningSupportedPaymentMethod is null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
LNURLPayRequest lnurlPayRequest;
|
||||||
|
var blob = store.GetStoreBlob();
|
||||||
|
if (i.Metadata.AdditionalData.TryGetValue("payRequest", out var t) && t is JObject jo)
|
||||||
{
|
{
|
||||||
|
lnurlPayRequest = jo.ToObject<LNURLPayRequest>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lnurlPayRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, allowOverpay: false);
|
||||||
|
if (lnurlPayRequest is null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var amt = amount.HasValue ? new LightMoney(amount.Value) : null;
|
if (amount is null)
|
||||||
var min = new LightMoney(isTopup ? 1m : accounting.Due.ToUnit(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
|
return Ok(lnurlPayRequest);
|
||||||
var max = isTopup ? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC) : min;
|
|
||||||
|
|
||||||
List<string[]> lnurlMetadata = new();
|
var amt = new LightMoney(amount.Value);
|
||||||
|
if (amt < lnurlPayRequest.MinSendable || amount > lnurlPayRequest.MaxSendable)
|
||||||
var blob = store.GetStoreBlob();
|
|
||||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
|
||||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
lnurlMetadata.Add(new[] { "text/plain", invoiceDescription });
|
|
||||||
if (!string.IsNullOrEmpty(paymentMethodDetails.ConsumedLightningAddress))
|
|
||||||
{
|
|
||||||
lnurlMetadata.Add(new[] { "text/identifier", paymentMethodDetails.ConsumedLightningAddress });
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = JsonConvert.SerializeObject(lnurlMetadata);
|
|
||||||
if (amt != null && (amt < min || amount > max))
|
|
||||||
{
|
|
||||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." });
|
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." });
|
||||||
}
|
|
||||||
|
|
||||||
LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null;
|
LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null;
|
||||||
if ((i.ReceiptOptions?.Enabled ?? blob.ReceiptOptions.Enabled) is true)
|
if ((i.ReceiptOptions?.Enabled ?? blob.ReceiptOptions.Enabled) is true)
|
||||||
@@ -574,22 +609,15 @@ namespace BTCPayServer
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amt is null)
|
bool updatePaymentMethod = false;
|
||||||
|
if (lnurlSupportedPaymentMethod.LUD12Enabled)
|
||||||
{
|
{
|
||||||
if (await _pluginHookService.ApplyFilter("modify-lnurlp-request", new LNURLPayRequest
|
comment = comment?.Truncate(2000);
|
||||||
|
if (paymentMethodDetails.ProvidedComment != comment)
|
||||||
{
|
{
|
||||||
Tag = "payRequest",
|
paymentMethodDetails.ProvidedComment = comment;
|
||||||
MinSendable = min,
|
updatePaymentMethod = true;
|
||||||
MaxSendable = max,
|
|
||||||
CommentAllowed = lnurlSupportedPaymentMethod.LUD12Enabled ? 2000 : 0,
|
|
||||||
Metadata = metadata,
|
|
||||||
Callback = new Uri(Request.GetCurrentUrl())
|
|
||||||
}) is not LNURLPayRequest lnurlp)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(lnurlp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amt)
|
if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amt)
|
||||||
@@ -613,11 +641,11 @@ namespace BTCPayServer
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
|
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
|
||||||
|
var metadata = JsonConvert.SerializeObject(lnurlPayRequest.Metadata);
|
||||||
var description = (await _pluginHookService.ApplyFilter("modify-lnurlp-description", metadata)) as string;
|
var description = (await _pluginHookService.ApplyFilter("modify-lnurlp-description", metadata)) as string;
|
||||||
if (description is null)
|
if (description is null)
|
||||||
{
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
var param = new CreateInvoiceParams(amt, description, expiry)
|
var param = new CreateInvoiceParams(amt, description, expiry)
|
||||||
{
|
{
|
||||||
PrivateRouteHints = blob.LightningPrivateRouteHints,
|
PrivateRouteHints = blob.LightningPrivateRouteHints,
|
||||||
@@ -649,30 +677,14 @@ namespace BTCPayServer
|
|||||||
paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage);
|
paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage);
|
||||||
paymentMethodDetails.InvoiceId = invoice.Id;
|
paymentMethodDetails.InvoiceId = invoice.Id;
|
||||||
paymentMethodDetails.GeneratedBoltAmount = amt;
|
paymentMethodDetails.GeneratedBoltAmount = amt;
|
||||||
if (lnurlSupportedPaymentMethod.LUD12Enabled)
|
updatePaymentMethod = true;
|
||||||
{
|
|
||||||
paymentMethodDetails.ProvidedComment = comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
|
||||||
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
|
|
||||||
|
|
||||||
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId,
|
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId,
|
||||||
paymentMethodDetails, pmi));
|
paymentMethodDetails, pmi));
|
||||||
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
|
|
||||||
{
|
|
||||||
Disposable = true,
|
|
||||||
Routes = Array.Empty<string>(),
|
|
||||||
Pr = paymentMethodDetails.BOLT11,
|
|
||||||
SuccessAction = successAction
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paymentMethodDetails.GeneratedBoltAmount == amt)
|
if (updatePaymentMethod)
|
||||||
{
|
{
|
||||||
if (lnurlSupportedPaymentMethod.LUD12Enabled && paymentMethodDetails.ProvidedComment != comment)
|
|
||||||
{
|
|
||||||
paymentMethodDetails.ProvidedComment = comment;
|
|
||||||
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
||||||
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
|
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
|
||||||
}
|
}
|
||||||
@@ -685,7 +697,6 @@ namespace BTCPayServer
|
|||||||
SuccessAction = successAction
|
SuccessAction = successAction
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest(new LNUrlStatusResponse
|
return BadRequest(new LNUrlStatusResponse
|
||||||
{
|
{
|
||||||
@@ -725,6 +736,7 @@ namespace BTCPayServer
|
|||||||
CurrencyCode = blob.CurrencyCode,
|
CurrencyCode = blob.CurrencyCode,
|
||||||
StoreId = storeId,
|
StoreId = storeId,
|
||||||
Username = s.Username,
|
Username = s.Username,
|
||||||
|
InvoiceMetadata = blob.InvoiceMetadata?.ToString(Formatting.Indented)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
).ToList()
|
).ToList()
|
||||||
@@ -746,6 +758,18 @@ namespace BTCPayServer
|
|||||||
vm.AddModelError(addressVm => addressVm.Add.CurrencyCode, "Currency is invalid", this);
|
vm.AddModelError(addressVm => addressVm.Add.CurrencyCode, "Currency is invalid", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JObject metadata = null;
|
||||||
|
if (!string.IsNullOrEmpty(vm.Add.InvoiceMetadata) )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
metadata = JObject.Parse(vm.Add.InvoiceMetadata);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
vm.AddModelError(addressVm => addressVm.Add.InvoiceMetadata, "Metadata must be a valid json object", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(vm);
|
return View(vm);
|
||||||
@@ -760,7 +784,8 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
Max = vm.Add.Max,
|
Max = vm.Add.Max,
|
||||||
Min = vm.Add.Min,
|
Min = vm.Add.Min,
|
||||||
CurrencyCode = vm.Add.CurrencyCode
|
CurrencyCode = vm.Add.CurrencyCode,
|
||||||
|
InvoiceMetadata = metadata
|
||||||
})))
|
})))
|
||||||
{
|
{
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
|
|||||||
@@ -67,26 +67,33 @@
|
|||||||
<div id="AdvancedSettings" class="collapse @(showAdvancedOptions ? "show" : "")">
|
<div id="AdvancedSettings" class="collapse @(showAdvancedOptions ? "show" : "")">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-sm-auto">
|
<div class="col-12 col-sm-auto">
|
||||||
<div class="form-group">
|
<div class="form-group" title="The currency to generate the invoice in when generated through this lightning address ">
|
||||||
<label asp-for="Add.CurrencyCode" class="form-label"></label>
|
<label asp-for="Add.CurrencyCode" class="form-label"></label>
|
||||||
<input asp-for="Add.CurrencyCode" class="form-control w-auto" currency-selection style="max-width:16ch;"/>
|
<input asp-for="Add.CurrencyCode" class="form-control w-auto" currency-selection style="max-width:16ch;"/>
|
||||||
<span asp-validation-for="Add.CurrencyCode" class="text-danger"></span>
|
<span asp-validation-for="Add.CurrencyCode" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-auto">
|
<div class="col-12 col-sm-auto">
|
||||||
<div class="form-group">
|
<div class="form-group" title="Minimum amount of sats to allow to be sent to this ln address">
|
||||||
<label asp-for="Add.Min" class="form-label"></label>
|
<label asp-for="Add.Min" class="form-label"></label>
|
||||||
<input asp-for="Add.Min" class="form-control" type="number" inputmode="numeric" min="1" style="max-width:16ch;"/>
|
<input asp-for="Add.Min" class="form-control" type="number" inputmode="numeric" min="1" style="max-width:16ch;"/>
|
||||||
<span asp-validation-for="Add.Min" class="text-danger"></span>
|
<span asp-validation-for="Add.Min" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-auto">
|
<div class="col-12 col-sm-auto">
|
||||||
<div class="form-group">
|
<div class="form-group" title="Maximum amount of sats to allow to be sent to this ln address">
|
||||||
<label asp-for="Add.Max" class="form-label"></label>
|
<label asp-for="Add.Max" class="form-label"></label>
|
||||||
<input asp-for="Add.Max" class="form-control" type="number" inputmode="numeric" min="1" max="@int.MaxValue" style="max-width:16ch;"/>
|
<input asp-for="Add.Max" class="form-control" type="number" inputmode="numeric" min="1" max="@int.MaxValue" style="max-width:16ch;"/>
|
||||||
<span asp-validation-for="Add.Max" class="text-danger"></span>
|
<span asp-validation-for="Add.Max" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<div class="form-group" title="Metadata (in JSON) to add to the invoice when created through this lightning address.">
|
||||||
|
<label asp-for="Add.InvoiceMetadata" class="form-label"></label>
|
||||||
|
<textarea asp-for="Add.InvoiceMetadata" class="form-control" ></textarea>
|
||||||
|
<span asp-validation-for="Add.InvoiceMetadata" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,6 +121,7 @@
|
|||||||
<input asp-for="Items[index].Min" type="hidden"/>
|
<input asp-for="Items[index].Min" type="hidden"/>
|
||||||
<input asp-for="Items[index].Max" type="hidden"/>
|
<input asp-for="Items[index].Max" type="hidden"/>
|
||||||
<input asp-for="Items[index].Username" type="hidden"/>
|
<input asp-for="Items[index].Username" type="hidden"/>
|
||||||
|
<input asp-for="Items[index].InvoiceMetadata" type="hidden"/>
|
||||||
var address = $"{Model.Items[index].Username}@{Context.Request.Host.ToUriComponent()}";
|
var address = $"{Model.Items[index].Username}@{Context.Request.Host.ToUriComponent()}";
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -138,6 +146,10 @@
|
|||||||
{
|
{
|
||||||
<span>tracked in @Model.Items[index].CurrencyCode</span>
|
<span>tracked in @Model.Items[index].CurrencyCode</span>
|
||||||
}
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Items[index].InvoiceMetadata))
|
||||||
|
{
|
||||||
|
<span>with invoice metadata @Model.Items[index].InvoiceMetadata</span>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button type="submit" title="Remove" name="command" value="@($"remove:{Model.Items[index].Username}")"
|
<button type="submit" title="Remove" name="command" value="@($"remove:{Model.Items[index].Username}")"
|
||||||
|
|||||||
Reference in New Issue
Block a user