diff --git a/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs b/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs index 3da5d2e..dca2f10 100644 --- a/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs +++ b/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Linq; using System.Text; using System.Threading; @@ -11,6 +12,7 @@ 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; @@ -23,7 +25,7 @@ namespace BTCPayServer.Plugins.NIP05 { public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = { - new() {Identifier = nameof(BTCPayServer), Condition = ">=1.7.7"} + new() {Identifier = nameof(BTCPayServer), Condition = ">=1.9.0"} }; public override void Execute(IServiceCollection applicationBuilder) @@ -35,26 +37,95 @@ namespace BTCPayServer.Plugins.NIP05 applicationBuilder.AddHostedService(); base.Execute(applicationBuilder); } + + public static string GetZapRequestCacheKey(string invoiceid) + { + 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) + 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) @@ -64,12 +135,11 @@ namespace BTCPayServer.Plugins.NIP05 { return; } - - var zapRequest = arg.Invoice.Metadata.GetAdditionalData("zapRequest"); - if (zapRequest is null) + 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); @@ -113,32 +183,7 @@ namespace BTCPayServer.Plugins.NIP05 await zapReceipt.ComputeIdAndSignAsync(key); - - await Task.WhenAll(relays.Concat(settings.settings.Relays?? Array.Empty()).Distinct().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)); - await c.ConnectAndWaitUntilConnected(cts.Token); - - c.OkReceived += (sender, okargs) => - { - if(okargs.eventId == zapReceipt.Id) - tcs.SetResult(); - }; - await c.PublishEvent(zapReceipt, cts.Token); - await tcs.Task.WaitAsync(cts.Token); - await c.Disconnect(); - } - catch (Exception e) - { - } - })); - + _pendingZapEvents.Add(new PendingZapEvent(relays.Concat(settings.settings.Relays?? Array.Empty()).Distinct().ToArray(), zapReceipt)); } public Task StopAsync(CancellationToken cancellationToken) @@ -192,16 +237,16 @@ namespace BTCPayServer.Plugins.NIP05 private readonly IHttpContextAccessor _httpContextAccessor; private readonly Nip5Controller _nip5Controller; private readonly LightningAddressService _lightningAddressService; - private readonly InvoiceRepository _invoiceRepository; + private readonly IMemoryCache _memoryCache; public LnurlDescriptionFilter(IHttpContextAccessor httpContextAccessor, Nip5Controller nip5Controller, LightningAddressService lightningAddressService, - InvoiceRepository invoiceRepository) + InvoiceRepository invoiceRepository, IMemoryCache memoryCache) { _httpContextAccessor = httpContextAccessor; _nip5Controller = nip5Controller; _lightningAddressService = lightningAddressService; - _invoiceRepository = invoiceRepository; + _memoryCache = memoryCache; } public override string Hook => "lnurlp-description"; @@ -236,19 +281,18 @@ namespace BTCPayServer.Plugins.NIP05 var parsedNote = System.Text.Json.JsonSerializer.Deserialize(nostr); if (parsedNote?.Kind != 9734) { - throw new InvalidOperationException("Invalid zap note, kind must be 9734"); + return arg; } if (!parsedNote.Verify()) { - throw new InvalidOperationException("Zap note sig check failed"); + return arg; } - var invoice = await _invoiceRepository.GetInvoice(invoiceId.ToString()); - - invoice.Metadata.SetAdditionalData("zapRequest", nostr); - await _invoiceRepository.UpdateInvoiceMetadata(invoiceId.ToString(), invoice.StoreId, - invoice.Metadata.ToJObject()); + var entry =_memoryCache.CreateEntry(Nip05Plugin.GetZapRequestCacheKey(invoiceId.ToString())); + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); + entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); + entry.SetValue(nostr); return nostr; } }