diff --git a/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj b/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
index 79d0a24..9a1e730 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.1.3
+ 1.1.4
true
@@ -36,7 +36,7 @@
-
+
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs b/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs
index 09bf812..8b317d7 100644
--- a/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs
+++ b/Plugins/BTCPayServer.Plugins.NIP05/Nip05Plugin.cs
@@ -2,6 +2,7 @@ using System.Text;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
+using BTCPayServer.Lightning;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.NIP05
@@ -17,10 +18,15 @@ namespace BTCPayServer.Plugins.NIP05
{
applicationBuilder.AddSingleton(new UIExtension("Nip05Nav",
"store-integrations-nav"));
+ applicationBuilder.AddSingleton(new UIExtension("NWC/LNPaymentMethodSetupTab", "ln-payment-method-setup-tab"));
+
applicationBuilder.AddSingleton();
applicationBuilder.AddSingleton();
applicationBuilder.AddSingleton();
applicationBuilder.AddHostedService(sp => sp.GetRequiredService());
+ applicationBuilder.AddSingleton();
+ applicationBuilder.AddSingleton(provider => provider.GetRequiredService());
+
base.Execute(applicationBuilder);
}
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/NostrClientPool.cs b/Plugins/BTCPayServer.Plugins.NIP05/NostrClientPool.cs
new file mode 100644
index 0000000..1e66750
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/NostrClientPool.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using NNostr.Client;
+using NNostr.Client.Protocols;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class NostrClientPool
+{
+ private static readonly ConcurrentDictionary _clientPool = new();
+
+ private static readonly Timer _cleanupTimer;
+
+ static NostrClientPool()
+ {
+ _cleanupTimer = new Timer(CleanupExpiredClients, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
+ }
+
+ public static (INostrClient, IDisposable) GetClient(string connstring)
+ {
+ var connParams = NIP47.ParseUri(new Uri(connstring));
+
+ var clientWrapper = _clientPool.GetOrAdd(connstring.ToString(),
+ k => new NostrClientWrapper(new CompositeNostrClient(connParams.relays)));
+
+ clientWrapper.IncrementUsage();
+
+ return (clientWrapper.Client, new UsageDisposable(clientWrapper));
+ }
+ public static async Task<(INostrClient, IDisposable)> GetClientAndConnect(string connstring, CancellationToken token)
+ {
+ var result = GetClient(connstring);
+
+ await result.Item1.ConnectAndWaitUntilConnected(token, CancellationToken.None);
+
+ return result;
+ }
+
+ public static void KillClient(string connstring)
+ {
+ if (_clientPool.TryRemove(connstring, out var clientWrapper))
+ {
+ clientWrapper.Dispose();
+ }
+ }
+
+ private static void CleanupExpiredClients(object state)
+ {
+ foreach (var key in _clientPool.Keys)
+ {
+ if (_clientPool[key].IsExpired())
+ {
+ if (_clientPool.TryRemove(key, out var clientWrapper))
+ {
+ clientWrapper.Dispose();
+ }
+ }
+ }
+ }
+
+ private class UsageDisposable : IDisposable
+ {
+ private readonly NostrClientWrapper _clientWrapper;
+
+ public UsageDisposable(NostrClientWrapper clientWrapper)
+ {
+ _clientWrapper = clientWrapper;
+ }
+
+ public void Dispose()
+ {
+ _clientWrapper.DecrementUsage();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/NostrClientWrapper.cs b/Plugins/BTCPayServer.Plugins.NIP05/NostrClientWrapper.cs
new file mode 100644
index 0000000..27efd89
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/NostrClientWrapper.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Threading;
+using NNostr.Client;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class NostrClientWrapper : IDisposable
+{
+ public INostrClient Client { get; private set; }
+ private int _usageCount = 0;
+ private bool _isDisposed = false;
+ private DateTimeOffset _lastUsed;
+
+ public NostrClientWrapper(INostrClient client)
+ {
+ Client = client;
+ _lastUsed = DateTimeOffset.UtcNow;
+ }
+
+ public void IncrementUsage()
+ {
+ _lastUsed = DateTimeOffset.UtcNow;
+ Interlocked.Increment(ref _usageCount);
+ }
+
+ public void DecrementUsage()
+ {
+ _lastUsed = DateTimeOffset.UtcNow;
+ if (Interlocked.Decrement(ref _usageCount) == 0 && IsExpired())
+ {
+ Dispose();
+ }
+ }
+
+ public bool IsExpired()
+ {
+ return DateTimeOffset.UtcNow - _lastUsed > TimeSpan.FromMinutes(5);
+ }
+
+ public void Dispose()
+ {
+ if (!_isDisposed)
+ {
+ Client.Dispose();
+ _isDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs b/Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs
new file mode 100644
index 0000000..c71e4e3
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs
@@ -0,0 +1,469 @@
+#nullable enable
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using BTCPayServer.Lightning;
+using NBitcoin;
+using NBitcoin.Secp256k1;
+using NNostr.Client;
+using NNostr.Client.Protocols;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class NostrWalletConnectLightningClient : ILightningClient
+{
+ private readonly Uri _uri;
+ private readonly Network _network;
+ private readonly (string[] Commands, string[] Notifications) _commands;
+ private readonly (ECXOnlyPubKey pubkey, ECPrivKey secret, Uri[] relays, string lud16) _connectParams;
+
+ public NostrWalletConnectLightningClient(Uri uri, Network network,
+ (string[] Commands, string[] Notifications) commands)
+ {
+ _uri = uri;
+ _network = network;
+ _commands = commands;
+ _connectParams = NIP47.ParseUri(uri);
+ }
+
+ public override string ToString()
+ {
+ return $"type=nwc;key={_uri}";
+ }
+
+
+ public async Task GetInvoice(string invoiceId,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ return await GetInvoice(uint256.Parse(invoiceId), cancellation);
+ }
+
+ private static LightningInvoice? ToLightningInvoice(NIP47.Nip47Transaction tx, Network network)
+ {
+ if (tx.Type != "incoming")
+ {
+ return null;
+ }
+
+ var isPaid = tx.SettledAt.HasValue;
+ var invoice = BOLT11PaymentRequest.Parse(tx.Invoice, network);
+ var expiresAt = tx.ExpiresAt is not null
+ ? DateTimeOffset.FromUnixTimeSeconds(tx.ExpiresAt.Value)
+ : invoice.ExpiryDate;
+ var expired = !isPaid && expiresAt < DateTimeOffset.UtcNow;
+ var s = tx.SettledAt is not null
+ ? DateTimeOffset.FromUnixTimeSeconds(tx.SettledAt.Value)
+ : (DateTimeOffset?) null;
+ return new LightningInvoice()
+ {
+ PaymentHash = tx.PaymentHash,
+ Amount = LightMoney.MilliSatoshis(tx.AmountMsats),
+ Preimage = tx.Preimage,
+ Id = tx.PaymentHash,
+ Status = isPaid ? LightningInvoiceStatus.Paid :
+ expired ? LightningInvoiceStatus.Expired : LightningInvoiceStatus.Unpaid,
+ PaidAt = s,
+ ExpiresAt = expiresAt,
+ AmountReceived = isPaid ? LightMoney.MilliSatoshis(tx.AmountMsats) : LightMoney.Zero,
+ BOLT11 = tx.Invoice
+ };
+ }
+
+
+ public async Task GetInvoice(uint256 paymentHash,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ var (nostrClient, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var tx = await nostrClient.SendNIP47Request(_connectParams.pubkey,
+ _connectParams.secret,
+ new NIP47.LookupInvoiceRequest()
+ {
+ PaymentHash = paymentHash.ToString()
+ }, cancellation);
+ return ToLightningInvoice(tx, _network)!;
+ }
+ }
+
+ public async Task ListInvoices(CancellationToken cancellation = new CancellationToken())
+ {
+ return await ListInvoices(new ListInvoicesParams(), cancellation);
+ }
+
+ public async Task ListInvoices(ListInvoicesParams request,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var response = await client.SendNIP47Request(_connectParams.pubkey,
+ _connectParams.secret,
+ new NIP47.ListTransactionsRequest()
+ {
+ Type = "incoming",
+ Offset = (int) (request.OffsetIndex ?? 0),
+ Unpaid = request.PendingOnly ?? false,
+ }, cancellation);
+
+ return response.Transactions.Select(transaction => ToLightningInvoice(transaction, _network))
+ .Where(i => i is not null).ToArray()!;
+ }
+ }
+
+
+ private LightningPayment? ToLightningPayment(NIP47.Nip47Transaction tx)
+ {
+ if (tx.Type != "outgoing")
+ {
+ return null;
+ }
+
+ var isPaid = tx.SettledAt.HasValue || !string.IsNullOrEmpty(tx.Preimage);
+ var invoice = BOLT11PaymentRequest.Parse(tx.Invoice, _network);
+ var expiresAt = tx.ExpiresAt is not null
+ ? DateTimeOffset.FromUnixTimeSeconds(tx.ExpiresAt.Value)
+ : invoice.ExpiryDate;
+ var created = DateTimeOffset.FromUnixTimeSeconds(tx.CreatedAt);
+ var expired = !isPaid && expiresAt < DateTimeOffset.UtcNow;
+ var s = tx.SettledAt is not null
+ ? DateTimeOffset.FromUnixTimeSeconds(tx.SettledAt.Value)
+ : (DateTimeOffset?) null;
+ return new LightningPayment()
+ {
+ PaymentHash = tx.PaymentHash,
+ Amount = LightMoney.MilliSatoshis(tx.AmountMsats),
+ Preimage = tx.Preimage,
+ Id = tx.PaymentHash,
+ Status = isPaid ? LightningPaymentStatus.Complete :
+ expired ? LightningPaymentStatus.Failed : LightningPaymentStatus.Unknown,
+ BOLT11 = tx.Invoice,
+ Fee = LightMoney.MilliSatoshis(tx.FeesPaidMsats),
+ AmountSent = LightMoney.MilliSatoshis(tx.AmountMsats + tx.FeesPaidMsats),
+ CreatedAt = created
+ };
+ }
+
+
+ public async Task GetPayment(string paymentHash,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var tx = await client.SendNIP47Request(_connectParams.pubkey, _connectParams.secret,
+ new NIP47.LookupInvoiceRequest()
+ {
+ PaymentHash = paymentHash
+ }, cancellation);
+ return ToLightningPayment(tx)!;
+ }
+ }
+
+ public async Task ListPayments(CancellationToken cancellation = new CancellationToken())
+ {
+ return await ListPayments(new ListPaymentsParams(), cancellation);
+ }
+
+ public async Task ListPayments(ListPaymentsParams request,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var response = await client.SendNIP47Request(_connectParams.pubkey,
+ _connectParams.secret,
+ new NIP47.ListTransactionsRequest()
+ {
+ Type = "outgoing",
+ Offset = (int) (request.OffsetIndex ?? 0),
+ Unpaid = request.IncludePending ?? false,
+ }, cancellation);
+ return response.Transactions.Select(ToLightningPayment).Where(i => i is not null).ToArray()!;
+ }
+ }
+
+ public async Task CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ return await CreateInvoice(new CreateInvoiceParams(amount, description, expiry), cancellation);
+ }
+
+ public async Task CreateInvoice(CreateInvoiceParams createInvoiceRequest,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var response = await client.SendNIP47Request(_connectParams.pubkey,
+ _connectParams.secret,
+ new NIP47.MakeInvoiceRequest()
+ {
+ AmountMsats = createInvoiceRequest.Amount.MilliSatoshi,
+ Description = createInvoiceRequest.Description is null || createInvoiceRequest.DescriptionHashOnly
+ ? null
+ : createInvoiceRequest.Description,
+ DescriptionHash = createInvoiceRequest.DescriptionHash?.ToString(),
+ ExpirySeconds = (int) createInvoiceRequest.Expiry.TotalSeconds,
+ }, cancellation);
+ return ToLightningInvoice(response, _network)!;
+ }
+ }
+
+ public async Task Listen(CancellationToken cancellation = new CancellationToken())
+ {
+ var x = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+ if (_commands.Notifications?.Contains("payment_received") is true)
+ {
+ return new NotificationListener(_network, x, _connectParams);
+ }
+
+ return new PollListener(_network, x, _connectParams);
+ }
+
+
+ public class NotificationListener : ILightningInvoiceListener
+ {
+ private readonly Network _network;
+ private readonly INostrClient _client;
+
+ private readonly CancellationTokenSource _cts;
+ private readonly IAsyncEnumerable _notifications;
+ private readonly IDisposable _disposable;
+
+ public NotificationListener(Network network, (INostrClient, IDisposable) client,
+ (ECXOnlyPubKey pubkey, ECPrivKey secret, Uri[] relays, string lud16) x)
+ {
+ _network = network;
+ _client = client.Item1;
+ _disposable = client.Item2;
+ _cts = new CancellationTokenSource();
+ _notifications = _client.SubscribeNip47Notifications(x.pubkey, x.secret, _cts.Token);
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _disposable.Dispose();
+ }
+
+ public async Task WaitInvoice(CancellationToken cancellation)
+ {
+ var enumerator = _notifications.GetAsyncEnumerator(cancellation);
+ while (await enumerator.MoveNextAsync(cancellation))
+ {
+ if (enumerator.Current.NotificationType == "payment_received")
+ {
+ var tx = enumerator.Current.Deserialize();
+ return ToLightningInvoice(tx, _network)!;
+ }
+ }
+
+ throw new Exception("No notification received");
+ }
+ }
+
+ public class PollListener : ILightningInvoiceListener
+ {
+ private readonly Network _network;
+ private readonly (ECXOnlyPubKey pubkey, ECPrivKey secret, Uri[] relays, string lud16) _connectparams;
+ private readonly INostrClient _client;
+
+ private readonly CancellationTokenSource _cts;
+ private readonly IAsyncEnumerable _notifications;
+ private readonly IDisposable _disposable;
+ private NIP47.ListTransactionsResponse? _lastPaid;
+ private Channel queue = Channel.CreateUnbounded();
+
+ public PollListener(Network network, (INostrClient, IDisposable) client,
+ (ECXOnlyPubKey pubkey, ECPrivKey secret, Uri[] relays, string lud16) connectparams)
+ {
+ _network = network;
+ _connectparams = connectparams;
+ _client = client.Item1;
+ _disposable = client.Item2;
+ _cts = new CancellationTokenSource();
+ _ = Poll();
+ }
+
+
+ private async Task Poll()
+ {
+ try
+ {
+ while (!_cts.IsCancellationRequested)
+ {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ cts.CancelAfter(TimeSpan.FromSeconds(10));
+ var paid = await _client.SendNIP47Request(_connectparams.pubkey,
+ _connectparams.secret, new NIP47.ListTransactionsRequest()
+ {
+ Type = "incoming",
+ // Unpaid = true, //seems like this is ignored... so we only get paid ones
+ Limit = 300
+ }, cancellationToken: cts.Token);
+ paid.Transactions = paid.Transactions.Where(i => i is {Type: "incoming", SettledAt: not null}).ToArray();
+ if (_lastPaid is not null)
+ {
+
+ var paidInvoicesSinceLastPoll = paid.Transactions.Where(i =>
+ _lastPaid.Transactions.All(j => j.PaymentHash != i.PaymentHash)).Select(i =>
+ ToLightningInvoice(i, _network)!);
+
+
+ //all invoices which are no longer in the unpaid list are paid
+ // var paidInvoicesSinceLastPoll = _lastPaid.Transactions
+ // .Where(i => paid.Transactions.All(j => j.PaymentHash != i.PaymentHash))
+ // .Select(i => ToLightningInvoice(i, _network)!);
+ foreach (var invoice in paidInvoicesSinceLastPoll)
+ {
+ await queue.Writer.WriteAsync(invoice,_cts.Token);
+ }
+ }
+
+ _lastPaid = paid;
+ await Task.Delay(1000, _cts.Token);
+ }
+ }
+ catch (Exception e)
+ {
+ Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _disposable.Dispose();
+ queue.Writer.Complete();
+ }
+
+ public async Task WaitInvoice(CancellationToken cancellation)
+ {
+ return await queue.Reader.ReadAsync(CancellationTokenSource
+ .CreateLinkedTokenSource(_cts.Token, cancellation).Token);
+ }
+ }
+
+ public async Task GetInfo(CancellationToken cancellation = new CancellationToken())
+ {
+ throw new NotSupportedException();
+ }
+
+ public async Task GetBalance(CancellationToken cancellation = new CancellationToken())
+ {
+ var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var response = await client.SendNIP47Request(_connectParams.pubkey,
+ _connectParams.secret,
+ new NIP47.NIP47Request("get_balance"), cancellation);
+ return new LightningNodeBalance()
+ {
+ OffchainBalance = new OffchainBalance()
+ {
+ Local = LightMoney.MilliSatoshis(response.BalanceMsats),
+ }
+ };
+ }
+ }
+
+ public async Task Pay(PayInvoiceParams payParams,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ return await Pay(null, new PayInvoiceParams(), cancellation);
+ }
+
+ public async Task Pay(string bolt11, PayInvoiceParams payParams,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ try
+ {
+ var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
+
+ using (usage)
+ {
+ var response = await client.SendNIP47Request(_connectParams.pubkey,
+ _connectParams.secret,
+ bolt11 is null
+ ? new NIP47.PayKeysendRequest()
+ {
+ Amount = Convert.ToDecimal(payParams.Amount.MilliSatoshi),
+ Pubkey = payParams.Destination.ToHex(),
+ TlvRecords = payParams.CustomRecords?.Select(kv => new NIP47.TlvRecord()
+ {
+ Type = kv.Key.ToString(),
+ Value = kv.Value
+ }).ToArray()
+ }
+ : new NIP47.PayInvoiceRequest()
+ {
+ Invoice = bolt11,
+ Amount = payParams.Amount?.MilliSatoshi is not null
+ ? Convert.ToDecimal(payParams.Amount.MilliSatoshi)
+ : null,
+ }, cancellation);
+ var lp = ToLightningPayment(response);
+ return new PayResponse(lp.Status == LightningPaymentStatus.Complete ? PayResult.Ok : PayResult.Error,
+ new PayDetails()
+ {
+ Preimage = lp.Preimage is null ? null : new uint256(lp.Preimage),
+ Status = lp.Status,
+ TotalAmount = lp.AmountSent,
+ PaymentHash = new uint256(lp.PaymentHash),
+ FeeAmount = lp.Fee
+ });
+ }
+ }
+ catch (Exception e)
+ {
+ return new PayResponse()
+ {
+ Result = PayResult.Error,
+ ErrorDetail = e.Message
+ };
+ }
+ }
+
+ public async Task Pay(string bolt11, CancellationToken cancellation = new CancellationToken())
+ {
+ return await Pay(bolt11, new PayInvoiceParams(), cancellation);
+ }
+
+ public async Task OpenChannel(OpenChannelRequest openChannelRequest,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ throw new NotSupportedException();
+ }
+
+ public async Task GetDepositAddress(CancellationToken cancellation = new CancellationToken())
+ {
+ throw new NotSupportedException();
+ }
+
+ public async Task ConnectTo(NodeInfo nodeInfo,
+ CancellationToken cancellation = new CancellationToken())
+ {
+ throw new NotSupportedException();
+ }
+
+ public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken())
+ {
+ throw new NotSupportedException();
+ }
+
+ public async Task ListChannels(CancellationToken cancellation = new CancellationToken())
+ {
+ throw new NotSupportedException();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningConnectionStringHandler.cs b/Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningConnectionStringHandler.cs
new file mode 100644
index 0000000..6f8735c
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningConnectionStringHandler.cs
@@ -0,0 +1,67 @@
+#nullable enable
+using System;
+using System.Linq;
+using System.Threading;
+using BTCPayServer.Lightning;
+using NBitcoin;
+using NNostr.Client.Protocols;
+
+namespace BTCPayServer.Plugins.NIP05;
+
+public class NostrWalletConnectLightningConnectionStringHandler : ILightningConnectionStringHandler
+{
+
+ public ILightningClient? Create(string connectionString, Network network, out string? error)
+ {
+
+
+ if (!connectionString.StartsWith(NIP47.UriScheme, StringComparison.OrdinalIgnoreCase) && !connectionString.StartsWith("type=nwc;key="))
+ {
+ error = null;
+ return null;
+ }
+
+ connectionString = connectionString.Replace("type=nwc;key=", "");
+ try
+ {
+ Uri.TryCreate(connectionString, UriKind.Absolute, out var uri);
+ var connectParams = NIP47.ParseUri(uri); var cts = new CancellationTokenSource();
+ cts.CancelAfter(TimeSpan.FromSeconds(10));
+ var (client, disposable) = NostrClientPool.GetClientAndConnect(connectionString, cts.Token).ConfigureAwait(false).GetAwaiter().GetResult();
+ using (disposable)
+ {
+ var commands = client.FetchNIP47AvailableCommands(connectParams.Item1, cancellationToken: cts.Token)
+ .ConfigureAwait(false).GetAwaiter().GetResult();
+ var requiredCommands = new[] {"get_info", "make_invoice", "lookup_invoice", "list_transactions"};
+ if (commands?.Commands is null || requiredCommands.Any(c => !commands.Value.Commands.Contains(c)))
+ {
+ error =
+ "No commands available or not all required commands are available (get_info, make_invoice, lookup_invoice, list_transactions)";
+ return null;
+ }
+
+ var response = client
+ .SendNIP47Request(connectParams.pubkey, connectParams.secret,
+ new NIP47.GetInfoRequest(), cancellationToken: cts.Token).ConfigureAwait(false).GetAwaiter()
+ .GetResult();
+
+ var walletNetwork = response.Network;
+ if (!network.ChainName.ToString().Equals(walletNetwork,
+ StringComparison.InvariantCultureIgnoreCase))
+ {
+ error =
+ $"The network of the wallet ({walletNetwork}) does not match the network of the server ({network.ChainName})";
+ return null;
+ }
+
+ error = null;
+ return new NostrWalletConnectLightningClient(uri, network, commands.Value);
+ }
+ }
+ catch (Exception e)
+ {
+ error = "Invalid nostr wallet connect uri";
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/Views/Shared/NWC/LNPaymentMethodSetupTab.cshtml b/Plugins/BTCPayServer.Plugins.NIP05/Views/Shared/NWC/LNPaymentMethodSetupTab.cshtml
new file mode 100644
index 0000000..a979d65
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.NIP05/Views/Shared/NWC/LNPaymentMethodSetupTab.cshtml
@@ -0,0 +1,41 @@
+@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
+@{
+ var storeId = Model.StoreId;
+ if (Model.CryptoCode != "BTC")
+ {
+ return;
+ }
+}
+
+
+
+
+
+
+
+
+
+ -
+
type=nwc;key=b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c
+
+ -
+
nostr+walletconnect:b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj
index 5702773..75fe703 100644
--- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj
+++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj
@@ -44,7 +44,7 @@
-
+
diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Nostr.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Nostr.cs
index f2a58fa..3070d5a 100644
--- a/Plugins/BTCPayServer.Plugins.Wabisabi/Nostr.cs
+++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Nostr.cs
@@ -59,7 +59,7 @@ public class Nostr
{
new() {TagIdentifier = EndpointTagIdentifier, Data = new List() {new Uri(coordinatorUri, "plugins/wabisabi-coordinator/").ToString()}},
new() {TagIdentifier = TypeTagIdentifier, Data = new List() { TypeTagValue}},
- new() {TagIdentifier = NetworkTagIdentifier, Data = new List() {currentNetwork.Name.ToLower()}}
+ new() {TagIdentifier = NetworkTagIdentifier, Data = new List() {currentNetwork.ChainName.ToString().ToLower()}}
}
};
@@ -83,12 +83,13 @@ public class Nostr
}
});
var result = new List();
- var network = currentNetwork.Name.ToLower();
+ var network = currentNetwork.ChainName.ToString().ToLower();
- var cts = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token,
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token,
cancellationToken);
await nostrClient.Connect(cts.Token);
+
result = await nostrClient.SubscribeForEvents(
new[]
{
@@ -98,7 +99,7 @@ public class Nostr
ExtensionData = new Dictionary()
{
["#type"] = JsonSerializer.SerializeToElement(new[] {TypeTagValue}),
- ["#network"] = JsonSerializer.SerializeToElement(new[] {network})
+ ["#network"] = JsonSerializer.SerializeToElement(new[] {network, currentNetwork.Name.ToLower()})
},
Limit = 1000
}
diff --git a/submodules/btcpayserver b/submodules/btcpayserver
index 4ebe468..3fbc717 160000
--- a/submodules/btcpayserver
+++ b/submodules/btcpayserver
@@ -1 +1 @@
-Subproject commit 4ebe46830b3293b533fd85be984160112e637890
+Subproject commit 3fbc717cd45e2d4a6766715163b1cea50db72f72