Fix: Using lnaddresses on Nostr should not result in lots of invoice being created

This commit is contained in:
nicolas.dorier 2023-06-24 23:14:54 +09:00 committed by Andrew Camilleri
parent db83d238d5
commit 4afec2e2b6
2 changed files with 83 additions and 14 deletions

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -2379,7 +2380,7 @@ namespace BTCPayServer.Tests
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value")); var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
Assert.Equal(2, addresses.Count); Assert.Equal(2, addresses.Count);
var callbacks = new List<Uri>();
foreach (IWebElement webElement in addresses) foreach (IWebElement webElement in addresses)
{ {
var value = webElement.GetAttribute("value"); var value = webElement.GetAttribute("value");
@ -2397,6 +2398,7 @@ namespace BTCPayServer.Tests
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));
callbacks.Add(request.Callback);
break; break;
case { } v when v.StartsWith(lnaddress1): case { } v when v.StartsWith(lnaddress1):
@ -2404,6 +2406,7 @@ namespace BTCPayServer.Tests
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));
callbacks.Add(request.Callback);
break; break;
default: default:
Assert.False(true, "Should have matched"); Assert.False(true, "Should have matched");
@ -2411,7 +2414,19 @@ namespace BTCPayServer.Tests
} }
} }
var repo = s.Server.PayTester.GetService<InvoiceRepository>(); var repo = s.Server.PayTester.GetService<InvoiceRepository>();
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } }); var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
// Resolving a ln address shouldn't create any btcpay invoice.
// This must be done because some NOST clients resolve ln addresses preemptively without user interaction
Assert.Empty(invoices);
// Calling the callbacks should create the invoices
foreach (var callback in callbacks)
{
using var r = await s.Server.PayTester.HttpClient.GetAsync(callback);
await r.Content.ReadAsStringAsync();
}
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length); Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}"; var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices) foreach (var i in invoices)

View file

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
@ -373,13 +374,52 @@ namespace BTCPayServer
return NotFound("Unknown username"); return NotFound("Unknown username");
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId); var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
var cryptoCode = "BTC";
if (store is null) if (store is null)
return NotFound("Unknown username"); return NotFound("Unknown username");
if (GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod) is null)
return NotFound("LNUrl not available for store");
var blob = lightningAddressSettings.GetBlob(); var blob = lightningAddressSettings.GetBlob();
return await GetLNURLRequest( var lnurlRequest = new LNURLPayRequest()
"BTC", {
Tag = "payRequest",
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
};
NormalizeSendable(lnurlRequest);
var lnUrlMetadata = new Dictionary<string, string>()
{
["text/identifier"] = $"{username}@{Request.Host}"
};
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, store.GetStoreBlob(), null);
lnurlRequest.Metadata =
JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
action: nameof(GetLNURLForLightningAddress),
controller: "UILNURL",
values: new { cryptoCode, username }, Request.Scheme, Request.Host, Request.PathBase));
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
return Ok(lnurlRequest);
}
[HttpGet("pay/lnaddress/{username}")]
[EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> GetLNURLForLightningAddress(string cryptoCode, string username, [FromQuery] long? amount = null, string comment = null)
{
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
if (lightningAddressSettings is null || username is null)
return NotFound("Unknown username");
var blob = lightningAddressSettings.GetBlob();
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
var result = await GetLNURLRequest(
cryptoCode,
store, store,
store.GetStoreBlob(), store.GetStoreBlob(),
new CreateInvoiceRequest() new CreateInvoiceRequest()
@ -396,6 +436,10 @@ namespace BTCPayServer
{ {
{ "text/identifier", $"{username}@{Request.Host}" } { "text/identifier", $"{username}@{Request.Host}" }
}); });
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest)
return result;
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
return await GetLNURLForInvoice(invoiceId, cryptoCode, amount, comment);
} }
@ -482,11 +526,7 @@ namespace BTCPayServer
if (!lnUrlMetadata.ContainsKey("text/plain")) if (!lnUrlMetadata.ContainsKey("text/plain"))
{ {
var invoiceDescription = blob.LightningDescriptionTemplate SetLNUrlDescriptionMetadata(lnUrlMetadata, store, blob, i.Metadata);
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lnUrlMetadata.Add("text/plain", invoiceDescription);
} }
lnurlRequest.Tag = "payRequest"; lnurlRequest.Tag = "payRequest";
@ -503,12 +543,7 @@ namespace BTCPayServer
lnurlRequest.MaxSendable = lnurlRequest.MinSendable; lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
} }
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat. NormalizeSendable(lnurlRequest);
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; lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
if (paymentMethodDetails.PayRequest is null) if (paymentMethodDetails.PayRequest is null)
@ -524,6 +559,25 @@ namespace BTCPayServer
return lnurlRequest; return lnurlRequest;
} }
private void SetLNUrlDescriptionMetadata(Dictionary<string, string> lnUrlMetadata, Data.StoreData store, StoreBlob blob, InvoiceMetadata invoiceMetadata)
{
var invoiceDescription = blob.LightningDescriptionTemplate
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoiceMetadata?.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoiceMetadata?.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lnUrlMetadata.Add("text/plain", invoiceDescription);
}
private static void NormalizeSendable(LNURLPayRequest lnurlRequest)
{
// 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);
}
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings) PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
{ {
lnUrlSettings = null; lnUrlSettings = null;