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