Zap support

This commit is contained in:
Kukks
2023-03-28 14:59:00 +02:00
parent e7e6041920
commit ffc333828b
6 changed files with 291 additions and 10 deletions

View File

@@ -9,9 +9,9 @@
<!-- Plugin specific properties --> <!-- Plugin specific properties -->
<PropertyGroup> <PropertyGroup>
<Product>Nostr NIP5</Product> <Product>Nostr </Product>
<Description>Allows you to verify your nostr account</Description> <Description>Allows you to verify your nostr account with NIP5 and zap like the rest of the crazies</Description>
<Version>1.0.3</Version> <Version>1.0.4</Version>
</PropertyGroup> </PropertyGroup>
<!-- Plugin development properties --> <!-- Plugin development properties -->
<PropertyGroup> <PropertyGroup>

View File

@@ -1,7 +1,21 @@
using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services; using BTCPayServer.Abstractions.Services;
using BTCPayServer.Events;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using LNURL;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using NNostr.Client;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace BTCPayServer.Plugins.NIP05 namespace BTCPayServer.Plugins.NIP05
{ {
@@ -11,12 +25,239 @@ namespace BTCPayServer.Plugins.NIP05
{ {
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.7.7"} new() {Identifier = nameof(BTCPayServer), Condition = ">=1.7.7"}
}; };
public override void Execute(IServiceCollection applicationBuilder) public override void Execute(IServiceCollection applicationBuilder)
{ {
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Nip05Nav", applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Nip05Nav",
"store-integrations-nav")); "store-integrations-nav"));
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlDescriptionFilter>();
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlFilter>();
applicationBuilder.AddHostedService<Zapper>();
base.Execute(applicationBuilder); base.Execute(applicationBuilder);
} }
} }
public class Zapper : IHostedService
{
private readonly EventAggregator _eventAggregator;
private readonly Nip5Controller _nip5Controller;
private IEventAggregatorSubscription _subscription;
public Zapper(EventAggregator eventAggregator, Nip5Controller nip5Controller)
{
_eventAggregator = eventAggregator;
_nip5Controller = nip5Controller;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_subscription = _eventAggregator.SubscribeAsync<InvoiceEvent>(Subscription);
return Task.CompletedTask;
}
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;
}
var zapRequest = arg.Invoice.Metadata.GetAdditionalData<string>("zapRequest");
if (zapRequest is null)
{
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<NostrEvent>(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);
await Task.WhenAll(relays.Concat(settings.settings.Relays?? Array.Empty<string>()).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)
{
}
}));
}
public Task StopAsync(CancellationToken cancellationToken)
{
_subscription?.Dispose();
return Task.CompletedTask;
}
}
public class LnurlFilter : PluginHookFilter<LNURLPayRequest>
{
private readonly Nip5Controller _nip5Controller;
private readonly LightningAddressService _lightningAddressService;
public override string Hook => "lnurlp";
public LnurlFilter(Nip5Controller nip5Controller, LightningAddressService lightningAddressService)
{
_nip5Controller = nip5Controller;
_lightningAddressService = lightningAddressService;
}
public override async Task<LNURLPayRequest> 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<string>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Nip5Controller _nip5Controller;
private readonly LightningAddressService _lightningAddressService;
private readonly InvoiceRepository _invoiceRepository;
public LnurlDescriptionFilter(IHttpContextAccessor httpContextAccessor,
Nip5Controller nip5Controller, LightningAddressService lightningAddressService,
InvoiceRepository invoiceRepository)
{
_httpContextAccessor = httpContextAccessor;
_nip5Controller = nip5Controller;
_lightningAddressService = lightningAddressService;
_invoiceRepository = invoiceRepository;
}
public override string Hook => "lnurlp-description";
public override async Task<string> 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<string[][]>(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<NostrEvent>(nostr);
if (parsedNote?.Kind != 9734)
{
throw new InvalidOperationException("Invalid zap note, kind must be 9734");
}
if (!parsedNote.Verify())
{
throw new InvalidOperationException("Zap note sig check failed");
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId.ToString());
invoice.Metadata.SetAdditionalData("zapRequest", nostr);
await _invoiceRepository.UpdateInvoiceMetadata(invoiceId.ToString(), invoice.StoreId,
invoice.Metadata.ToJObject());
return nostr;
}
}
}
catch (Exception e)
{
}
return arg;
}
}
} }

View File

@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using NBitcoin.Secp256k1;
using NNostr.Client; using NNostr.Client;
using NNostr.Client.Protocols; using NNostr.Client.Protocols;
@@ -85,6 +86,38 @@ public class Nip5Controller : Controller
ModelState.AddModelError(nameof(settings.PubKey), "invalid public key"); ModelState.AddModelError(nameof(settings.PubKey), "invalid public key");
} }
if (!string.IsNullOrEmpty(settings.PrivateKey))
{
try
{
ECPrivKey k;
try
{
k = settings.PrivateKey.FromNIP19Nsec();
}
catch (Exception e)
{
k = NostrExtensions.ParseKey(settings.PrivateKey);
}
if (string.IsNullOrEmpty(settings.PubKey))
{
settings.PubKey = k.CreateXOnlyPubKey().ToHex();
ModelState.Remove(nameof(settings.PubKey));
}
else if(settings.PubKey != k.CreateXOnlyPubKey().ToHex())
ModelState.AddModelError(nameof(settings.PrivateKey), "private key does not match public key provided. Clear the public key to generate it from the private key.");
}
catch (Exception e)
{
ModelState.AddModelError(nameof(settings.PubKey), "invalid private key");
}
}
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(settings); return View(settings);
@@ -109,8 +142,8 @@ public class Nip5Controller : Controller
await _storeRepository.UpdateSetting(storeId, "NIP05", settings); await _storeRepository.UpdateSetting(storeId, "NIP05", settings);
return RedirectToAction("Edit", new {storeId}); return RedirectToAction("Edit", new {storeId});
} }
[NonAction]
private async Task<(string? storeId, Nip5StoreSettings? settings)> Get(string name) public async Task<(string? storeId, Nip5StoreSettings? settings)> Get(string name)
{ {
var rex = await _memoryCache.GetOrCreateAsync<(string? storeId, Nip5StoreSettings? settings)>( var rex = await _memoryCache.GetOrCreateAsync<(string? storeId, Nip5StoreSettings? settings)>(
$"NIP05_{name.ToLowerInvariant()}", $"NIP05_{name.ToLowerInvariant()}",

View File

@@ -8,6 +8,7 @@ namespace BTCPayServer.Plugins.NIP05
public class Nip5StoreSettings public class Nip5StoreSettings
{ {
[Required] public string PubKey { get; set; } [Required] public string PubKey { get; set; }
public string PrivateKey { get; set; }
[Required] public string Name { get; set; } [Required] public string Name { get; set; }
public string[]? Relays { get; set; } public string[]? Relays { get; set; }

View File

@@ -2,7 +2,7 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@model Nip5StoreSettings @model Nip5StoreSettings
@{ @{
ViewData.SetActivePage("Nostr NIP05", "Nostr NIP05", "Nostr NIP05"); ViewData.SetActivePage("Nostr", "Nostr", "Nostr");
} }
<partial name="_StatusMessage"/> <partial name="_StatusMessage"/>
@@ -23,6 +23,12 @@
<input asp-for="PubKey" class="form-control"/> <input asp-for="PubKey" class="form-control"/>
<span asp-validation-for="PubKey" class="text-danger"></span> <span asp-validation-for="PubKey" class="text-danger"></span>
</div> </div>
<div class="form-group">
<label asp-for="PrivateKey" class="form-label">Private key for zaps</label>
<input asp-for="PrivateKey" class="form-control"/>
<span asp-validation-for="PubKey" class="text-danger"></span>
<p class="text-muted">You'll also need to enable a lightning address with the same name.</p>
</div>
</div> </div>
<div class="row"> <div class="row">
<table class="table table-responsive col-12"> <table class="table table-responsive col-12">