diff --git a/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj b/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
index 0eb6d77..0be3042 100644
--- a/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
+++ b/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
@@ -11,7 +11,7 @@
Nostr
Allows you to verify your nostr account with NIP5 and zap like the rest of the crazies
- 1.0.5
+ 1.0.6
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/LnurlDescriptionFilter.cs b/Plugins/BTCPayServer.Plugins.NIP05/LnurlDescriptionFilter.cs
new file mode 100644
index 0000000..0e3429c
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/LnurlDescriptionFilter.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Services;
+using BTCPayServer.Services.Invoices;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using NNostr.Client;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class LnurlDescriptionFilter : PluginHookFilter
+{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly Nip5Controller _nip5Controller;
+ private readonly LightningAddressService _lightningAddressService;
+ private readonly IMemoryCache _memoryCache;
+ private readonly ILogger _logger;
+
+ public LnurlDescriptionFilter(IHttpContextAccessor httpContextAccessor,
+ Nip5Controller nip5Controller, LightningAddressService lightningAddressService,
+ InvoiceRepository invoiceRepository, IMemoryCache memoryCache, ILogger logger)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ _nip5Controller = nip5Controller;
+ _lightningAddressService = lightningAddressService;
+ _memoryCache = memoryCache;
+ _logger = logger;
+ }
+
+ public override string Hook => "modify-lnurlp-description";
+
+ public override async Task Execute(string arg)
+ {
+
+ if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue("nostr", out var nostr) &&
+ _httpContextAccessor.HttpContext.Request.RouteValues.TryGetValue("invoiceId", out var invoiceId))
+ {
+ try
+ {
+ var metadata = JsonConvert.DeserializeObject(arg);
+ var username = metadata
+ .FirstOrDefault(strings => strings.FirstOrDefault()?.Equals("text/identifier") is true)
+ ?.FirstOrDefault()?.ToLowerInvariant().Split("@")[0];
+ if (string.IsNullOrEmpty(username))
+ {
+ return arg;
+ }
+
+ var lnAddress = await _lightningAddressService.ResolveByAddress(username);
+ if (lnAddress is null)
+ {
+ return arg;
+ }
+
+ var user = await _nip5Controller.Get(username);
+ if (user.storeId is not null)
+ {
+ if (user.storeId != lnAddress.StoreDataId)
+ {
+ return arg;
+ }
+
+ var parsedNote = System.Text.Json.JsonSerializer.Deserialize(nostr);
+ if (parsedNote?.Kind != 9734)
+ {
+ return arg;
+ }
+
+ if (!parsedNote.Verify())
+ {
+ return arg;
+ }
+
+ var entry = _memoryCache.CreateEntry(Nip05Plugin.GetZapRequestCacheKey(invoiceId.ToString()));
+ entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
+ entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
+ entry.SetValue(nostr);
+ return nostr;
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, $"Error while processing nostr zap request for invoice {invoiceId}\n{nostr}");
+ }
+ }
+
+
+
+ return arg;
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/LnurlFilter.cs b/Plugins/BTCPayServer.Plugins.NIP05/LnurlFilter.cs
new file mode 100644
index 0000000..ab55cca
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/LnurlFilter.cs
@@ -0,0 +1,45 @@
+using System.Linq;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Services;
+using LNURL;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class LnurlFilter : PluginHookFilter
+{
+ private readonly Nip5Controller _nip5Controller;
+ private readonly LightningAddressService _lightningAddressService;
+ public override string Hook => "modify-lnurlp-request";
+
+ public LnurlFilter(Nip5Controller nip5Controller, LightningAddressService lightningAddressService)
+ {
+ _nip5Controller = nip5Controller;
+ _lightningAddressService = lightningAddressService;
+ }
+
+ public override async Task Execute(LNURLPayRequest arg)
+ {
+ var name = arg.ParsedMetadata.FirstOrDefault(pair => pair.Key == "text/identifier").Value
+ ?.ToLowerInvariant().Split("@")[0];
+ if (string.IsNullOrEmpty(name))
+ {
+ return arg;
+ }
+
+ var lnAddress = await _lightningAddressService.ResolveByAddress(name);
+ if (lnAddress is null)
+ {
+ return arg;
+ }
+
+ var nip5 = await _nip5Controller.Get(name);
+ if (nip5.storeId != lnAddress.StoreDataId)
+ {
+ return arg;
+ }
+
+ arg.NostrPubkey = nip5.settings.PubKey;
+ arg.AllowsNostr = true;
+ return arg;
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs b/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs
index 8e95978..21b26ff 100644
--- a/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs
+++ b/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs
@@ -1,23 +1,8 @@
-using System;
-using System.Collections.Concurrent;
-using System.Linq;
using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
-using BTCPayServer.Events;
-using BTCPayServer.Payments;
-using BTCPayServer.Services.Invoices;
-using LNURL;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Newtonsoft.Json;
-using NNostr.Client;
-using JsonSerializer = System.Text.Json.JsonSerializer;
namespace BTCPayServer.Plugins.NIP05
{
@@ -43,265 +28,4 @@ namespace BTCPayServer.Plugins.NIP05
return nameof(Nip05Plugin)+ invoiceid;
}
}
-
- public class Zapper : IHostedService
- {
- record PendingZapEvent(string[] relays, NostrEvent nostrEvent);
-
- private readonly EventAggregator _eventAggregator;
- private readonly Nip5Controller _nip5Controller;
- private readonly IMemoryCache _memoryCache;
- private IEventAggregatorSubscription _subscription;
- private ConcurrentBag _pendingZapEvents = new();
-
- public Zapper(EventAggregator eventAggregator, Nip5Controller nip5Controller, IMemoryCache memoryCache)
- {
- _eventAggregator = eventAggregator;
- _nip5Controller = nip5Controller;
- _memoryCache = memoryCache;
- }
-
- public Task StartAsync(CancellationToken cancellationToken)
- {
- _subscription = _eventAggregator.SubscribeAsync(Subscription);
- _ = SendZapReceipts(cancellationToken);
- return Task.CompletedTask;
- }
-
- private async Task SendZapReceipts(CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
-
- if (_pendingZapEvents.Any())
- {
- var pendingZaps = _pendingZapEvents.Take(Range.All).ToArray();
- var relaysToConnectTo = pendingZaps.SelectMany(@event => @event.relays).Distinct();
- var relaysToZap =relaysToConnectTo.ToDictionary(s => s, s => pendingZaps.Where(@event => @event.relays.Contains(s)).Select(@event => @event.nostrEvent).ToArray());
-
- await Task.WhenAll(relaysToZap.Select(async relay =>
- {
- try
- {
-
- var cts = new CancellationTokenSource();
- cts.CancelAfter(TimeSpan.FromSeconds(30));
- var tcs = new TaskCompletionSource();
- using var c = new NostrClient(new Uri(relay.Key));
- await c.ConnectAndWaitUntilConnected(cts.Token);
- var pendingOksOnIds = relay.Value.Select(a => a.Id).ToHashSet();
- c.OkReceived += (sender, okargs) =>
- {
- pendingOksOnIds.Remove(okargs.eventId);
- if(!pendingOksOnIds.Any())
- tcs.SetResult();
- };
- foreach (var nostrEvent in relay.Value)
- {
- await c.PublishEvent(nostrEvent, cts.Token);
-
- }
- await tcs.Task.WaitAsync(cts.Token);
- await c.Disconnect();
- }
- catch (Exception e)
- {
- }
- }));
-
- }
- var waitingToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- waitingToken.CancelAfter(TimeSpan.FromMinutes(2));
- while (!waitingToken.IsCancellationRequested)
- {
- if (_pendingZapEvents.Count > 10)
- {
- waitingToken.Cancel();
- }
- else
- {
- await Task.Delay(100, waitingToken.Token);
- }
- }
- }
- }
-
- private async Task Subscription(InvoiceEvent arg)
- {
- if (arg.EventCode != InvoiceEventCode.Completed)
- return;
- var pm = arg.Invoice.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
- if (pm is null)
- {
- return;
- }
- if(!_memoryCache.TryGetValue(Nip05Plugin.GetZapRequestCacheKey(arg.Invoice.Id), out var zapRequestEntry) || zapRequestEntry is not string zapRequest)
- {
- return;
- }
-
- var pmd = (LNURLPayPaymentMethodDetails) pm.GetPaymentMethodDetails();
- var name = pmd.ConsumedLightningAddress.Split("@")[0];
- var settings = await _nip5Controller.Get(name);
- if (settings.storeId != arg.Invoice.StoreId)
- {
- return;
- }
-
- if (string.IsNullOrEmpty(settings.settings.PrivateKey))
- {
- return;
- }
-
- var key = NostrExtensions.ParseKey(settings.settings.PrivateKey);
-
- var zapRequestEvent = JsonSerializer.Deserialize(zapRequest);
- var relays = zapRequestEvent.GetTaggedData("relays");
-
- var tags = zapRequestEvent.Tags.Where(a => a.TagIdentifier.Length == 1).ToList();
- tags.Add(new()
- {
- TagIdentifier = "bolt11",
- Data = new() {pmd.BOLT11}
- });
-
- tags.Add(new()
- {
- TagIdentifier = "description",
- Data = new() {zapRequest}
- });
-
- var zapReceipt = new NostrEvent()
- {
- Kind = 9735,
- CreatedAt = DateTimeOffset.UtcNow,
- PublicKey = settings.settings.PubKey,
- Content = zapRequestEvent.Content,
- Tags = tags
- };
-
-
- await zapReceipt.ComputeIdAndSignAsync(key);
-
- _pendingZapEvents.Add(new PendingZapEvent(relays.Concat(settings.settings.Relays?? Array.Empty()).Distinct().ToArray(), zapReceipt));
- }
-
- public Task StopAsync(CancellationToken cancellationToken)
- {
- _subscription?.Dispose();
- return Task.CompletedTask;
- }
- }
-
- public class LnurlFilter : PluginHookFilter
- {
- private readonly Nip5Controller _nip5Controller;
- private readonly LightningAddressService _lightningAddressService;
- public override string Hook => "modify-lnurlp-request";
-
- public LnurlFilter(Nip5Controller nip5Controller, LightningAddressService lightningAddressService)
- {
- _nip5Controller = nip5Controller;
- _lightningAddressService = lightningAddressService;
- }
-
- public override async Task Execute(LNURLPayRequest arg)
- {
- var name = arg.ParsedMetadata.FirstOrDefault(pair => pair.Key == "text/identifier").Value
- ?.ToLowerInvariant().Split("@")[0];
- if (string.IsNullOrEmpty(name))
- {
- return arg;
- }
-
- var lnAddress = await _lightningAddressService.ResolveByAddress(name);
- if (lnAddress is null)
- {
- return arg;
- }
-
- var nip5 = await _nip5Controller.Get(name);
- if (nip5.storeId != lnAddress.StoreDataId)
- {
- return arg;
- }
-
- arg.NostrPubkey = nip5.settings.PubKey;
- arg.AllowsNostr = true;
- return arg;
- }
- }
-
- public class LnurlDescriptionFilter : PluginHookFilter
- {
- private readonly IHttpContextAccessor _httpContextAccessor;
- private readonly Nip5Controller _nip5Controller;
- private readonly LightningAddressService _lightningAddressService;
- private readonly IMemoryCache _memoryCache;
-
- public LnurlDescriptionFilter(IHttpContextAccessor httpContextAccessor,
- Nip5Controller nip5Controller, LightningAddressService lightningAddressService,
- InvoiceRepository invoiceRepository, IMemoryCache memoryCache)
- {
- _httpContextAccessor = httpContextAccessor;
- _nip5Controller = nip5Controller;
- _lightningAddressService = lightningAddressService;
- _memoryCache = memoryCache;
- }
-
- public override string Hook => "modify-lnurlp-description";
-
- public override async Task Execute(string arg)
- {
- try
- {
- if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue("nostr", out var nostr) &&
- _httpContextAccessor.HttpContext.Request.RouteValues.TryGetValue("invoiceId", out var invoiceId))
- {
- var metadata = JsonConvert.DeserializeObject(arg);
- var username = metadata
- .FirstOrDefault(strings => strings.FirstOrDefault()?.Equals("text/identifier") is true)
- ?.FirstOrDefault()?.ToLowerInvariant().Split("@")[0];
- if (string.IsNullOrEmpty(username))
- {
- return arg;
- }
-
- var lnAddress = await _lightningAddressService.ResolveByAddress(username);
-
-
- var user = await _nip5Controller.Get(username);
- if (user.storeId is not null)
- {
- if (user.storeId != lnAddress.StoreDataId)
- {
- return arg;
- }
-
- var parsedNote = System.Text.Json.JsonSerializer.Deserialize(nostr);
- if (parsedNote?.Kind != 9734)
- {
- return arg;
- }
-
- if (!parsedNote.Verify())
- {
- return arg;
- }
-
- var entry =_memoryCache.CreateEntry(Nip05Plugin.GetZapRequestCacheKey(invoiceId.ToString()));
- entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
- entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
- entry.SetValue(nostr);
- return nostr;
- }
- }
- }
- catch (Exception e)
- {
- }
-
- return arg;
- }
- }
}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs b/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs
new file mode 100644
index 0000000..3930d13
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using BTCPayServer.Events;
+using BTCPayServer.Payments;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NNostr.Client;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class Zapper : IHostedService
+{
+ record PendingZapEvent(string[] relays, NostrEvent nostrEvent);
+
+ private readonly EventAggregator _eventAggregator;
+ private readonly Nip5Controller _nip5Controller;
+ private readonly IMemoryCache _memoryCache;
+ private readonly ILogger _logger;
+ private IEventAggregatorSubscription _subscription;
+ private ConcurrentBag _pendingZapEvents = new();
+
+ public Zapper(EventAggregator eventAggregator, Nip5Controller nip5Controller, IMemoryCache memoryCache, ILogger logger)
+ {
+ _eventAggregator = eventAggregator;
+ _nip5Controller = nip5Controller;
+ _memoryCache = memoryCache;
+ _logger = logger;
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _subscription = _eventAggregator.SubscribeAsync(Subscription);
+ _ = SendZapReceipts(cancellationToken);
+ return Task.CompletedTask;
+ }
+
+ private async Task SendZapReceipts(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+
+ if (_pendingZapEvents.Any())
+ {
+ _logger.LogInformation($"Attempting to send {_pendingZapEvents.Count} zap receipts");
+ var pendingZaps = _pendingZapEvents.Take(Range.All).ToArray();
+ var relaysToConnectTo = pendingZaps.SelectMany(@event => @event.relays).Distinct();
+ var relaysToZap =relaysToConnectTo.ToDictionary(s => s, s => pendingZaps.Where(@event => @event.relays.Contains(s)).Select(@event => @event.nostrEvent).ToArray());
+
+ await Task.WhenAll(relaysToZap.Select(async relay =>
+ {
+ try
+ {
+
+ _logger.LogInformation($"Zapping {relay.Value.Length} to {relay.Key}");
+ var cts = new CancellationTokenSource();
+ cts.CancelAfter(TimeSpan.FromSeconds(30));
+ var tcs = new TaskCompletionSource();
+ using var c = new NostrClient(new Uri(relay.Key));
+ await c.ConnectAndWaitUntilConnected(cts.Token);
+ var pendingOksOnIds = relay.Value.Select(a => a.Id).ToHashSet();
+ c.OkReceived += (sender, okargs) =>
+ {
+ pendingOksOnIds.Remove(okargs.eventId);
+ if(!pendingOksOnIds.Any())
+ tcs.SetResult();
+ };
+ foreach (var nostrEvent in relay.Value)
+ {
+ await c.PublishEvent(nostrEvent, cts.Token);
+
+ }
+ await tcs.Task.WaitAsync(cts.Token);
+ await c.Disconnect();
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, $"Error zapping to {relay.Key}");
+ }
+ }));
+
+ }
+ var waitingToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ waitingToken.CancelAfter(TimeSpan.FromMinutes(2));
+ while (!waitingToken.IsCancellationRequested)
+ {
+ if (_pendingZapEvents.Count > 10)
+ {
+ waitingToken.Cancel();
+ }
+ else
+ {
+ await Task.Delay(100, waitingToken.Token);
+ }
+ }
+ }
+ }
+
+ private async Task Subscription(InvoiceEvent arg)
+ {
+ if (arg.EventCode != InvoiceEventCode.Completed)
+ return;
+ var pm = arg.Invoice.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
+ if (pm is null)
+ {
+ return;
+ }
+ if(!_memoryCache.TryGetValue(Nip05Plugin.GetZapRequestCacheKey(arg.Invoice.Id), out var zapRequestEntry) || zapRequestEntry is not string zapRequest)
+ {
+ return;
+ }
+
+ var pmd = (LNURLPayPaymentMethodDetails) pm.GetPaymentMethodDetails();
+ var name = pmd.ConsumedLightningAddress.Split("@")[0];
+ var settings = await _nip5Controller.Get(name);
+ if (settings.storeId != arg.Invoice.StoreId)
+ {
+ return;
+ }
+
+ if (string.IsNullOrEmpty(settings.settings.PrivateKey))
+ {
+ return;
+ }
+
+ var key = NostrExtensions.ParseKey(settings.settings.PrivateKey);
+
+ var zapRequestEvent = JsonSerializer.Deserialize(zapRequest);
+ var relays = zapRequestEvent.GetTaggedData("relays");
+
+ var tags = zapRequestEvent.Tags.Where(a => a.TagIdentifier.Length == 1).ToList();
+ tags.Add(new()
+ {
+ TagIdentifier = "bolt11",
+ Data = new() {pmd.BOLT11}
+ });
+
+ tags.Add(new()
+ {
+ TagIdentifier = "description",
+ Data = new() {zapRequest}
+ });
+
+ var zapReceipt = new NostrEvent()
+ {
+ Kind = 9735,
+ CreatedAt = DateTimeOffset.UtcNow,
+ PublicKey = settings.settings.PubKey,
+ Content = zapRequestEvent.Content,
+ Tags = tags
+ };
+
+
+ await zapReceipt.ComputeIdAndSignAsync(key);
+
+ _pendingZapEvents.Add(new PendingZapEvent(relays.Concat(settings.settings.Relays?? Array.Empty()).Distinct().ToArray(), zapReceipt));
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _subscription?.Dispose();
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file