mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 15:44:26 +01:00
add nwc and bump nostr
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Product>Nostr </Product>
|
<Product>Nostr </Product>
|
||||||
<Description>Allows you to verify your nostr account with NIP5 and zap like the rest of the crazies</Description>
|
<Description>Allows you to verify your nostr account with NIP5 and zap like the rest of the crazies</Description>
|
||||||
<Version>1.1.3</Version>
|
<Version>1.1.4</Version>
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Plugin development properties -->
|
<!-- Plugin development properties -->
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NNostr.Client" Version="0.0.38" />
|
<PackageReference Include="NNostr.Client" Version="0.0.43" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources" />
|
<Folder Include="Resources" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Text;
|
|||||||
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.Lightning;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.NIP05
|
namespace BTCPayServer.Plugins.NIP05
|
||||||
@@ -17,10 +18,15 @@ namespace BTCPayServer.Plugins.NIP05
|
|||||||
{
|
{
|
||||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Nip05Nav",
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Nip05Nav",
|
||||||
"store-integrations-nav"));
|
"store-integrations-nav"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NWC/LNPaymentMethodSetupTab", "ln-payment-method-setup-tab"));
|
||||||
|
|
||||||
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlDescriptionFilter>();
|
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlDescriptionFilter>();
|
||||||
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlFilter>();
|
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlFilter>();
|
||||||
applicationBuilder.AddSingleton<Zapper>();
|
applicationBuilder.AddSingleton<Zapper>();
|
||||||
applicationBuilder.AddHostedService(sp => sp.GetRequiredService<Zapper>());
|
applicationBuilder.AddHostedService(sp => sp.GetRequiredService<Zapper>());
|
||||||
|
applicationBuilder.AddSingleton<NostrWalletConnectLightningConnectionStringHandler>();
|
||||||
|
applicationBuilder.AddSingleton<ILightningConnectionStringHandler>(provider => provider.GetRequiredService<NostrWalletConnectLightningConnectionStringHandler>());
|
||||||
|
|
||||||
base.Execute(applicationBuilder);
|
base.Execute(applicationBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
77
Plugins/BTCPayServer.Plugins.NIP05/NostrClientPool.cs
Normal file
77
Plugins/BTCPayServer.Plugins.NIP05/NostrClientPool.cs
Normal file
@@ -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<string, NostrClientWrapper> _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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Plugins/BTCPayServer.Plugins.NIP05/NostrClientWrapper.cs
Normal file
48
Plugins/BTCPayServer.Plugins.NIP05/NostrClientWrapper.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<LightningInvoice> 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<LightningInvoice> GetInvoice(uint256 paymentHash,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
var (nostrClient, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
|
||||||
|
|
||||||
|
using (usage)
|
||||||
|
{
|
||||||
|
var tx = await nostrClient.SendNIP47Request<NIP47.Nip47Transaction>(_connectParams.pubkey,
|
||||||
|
_connectParams.secret,
|
||||||
|
new NIP47.LookupInvoiceRequest()
|
||||||
|
{
|
||||||
|
PaymentHash = paymentHash.ToString()
|
||||||
|
}, cancellation);
|
||||||
|
return ToLightningInvoice(tx, _network)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningInvoice[]> ListInvoices(CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
return await ListInvoices(new ListInvoicesParams(), cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
|
||||||
|
|
||||||
|
using (usage)
|
||||||
|
{
|
||||||
|
var response = await client.SendNIP47Request<NIP47.ListTransactionsResponse>(_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<LightningPayment> GetPayment(string paymentHash,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
|
||||||
|
|
||||||
|
using (usage)
|
||||||
|
{
|
||||||
|
var tx = await client.SendNIP47Request<NIP47.Nip47Transaction>(_connectParams.pubkey, _connectParams.secret,
|
||||||
|
new NIP47.LookupInvoiceRequest()
|
||||||
|
{
|
||||||
|
PaymentHash = paymentHash
|
||||||
|
}, cancellation);
|
||||||
|
return ToLightningPayment(tx)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningPayment[]> ListPayments(CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
return await ListPayments(new ListPaymentsParams(), cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
|
||||||
|
|
||||||
|
using (usage)
|
||||||
|
{
|
||||||
|
var response = await client.SendNIP47Request<NIP47.ListTransactionsResponse>(_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<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
return await CreateInvoice(new CreateInvoiceParams(amount, description, expiry), cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
|
||||||
|
|
||||||
|
using (usage)
|
||||||
|
{
|
||||||
|
var response = await client.SendNIP47Request<NIP47.Nip47Transaction>(_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<ILightningInvoiceListener> 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<NIP47.Nip47Notification> _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<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
var enumerator = _notifications.GetAsyncEnumerator(cancellation);
|
||||||
|
while (await enumerator.MoveNextAsync(cancellation))
|
||||||
|
{
|
||||||
|
if (enumerator.Current.NotificationType == "payment_received")
|
||||||
|
{
|
||||||
|
var tx = enumerator.Current.Deserialize<NIP47.Nip47Transaction>();
|
||||||
|
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<NIP47.Nip47Notification> _notifications;
|
||||||
|
private readonly IDisposable _disposable;
|
||||||
|
private NIP47.ListTransactionsResponse? _lastPaid;
|
||||||
|
private Channel<LightningInvoice> queue = Channel.CreateUnbounded<LightningInvoice>();
|
||||||
|
|
||||||
|
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<NIP47.ListTransactionsResponse>(_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<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
return await queue.Reader.ReadAsync(CancellationTokenSource
|
||||||
|
.CreateLinkedTokenSource(_cts.Token, cancellation).Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
var (client, usage) = await NostrClientPool.GetClientAndConnect(_uri.ToString(), cancellation);
|
||||||
|
|
||||||
|
using (usage)
|
||||||
|
{
|
||||||
|
var response = await client.SendNIP47Request<NIP47.GetBalanceResponse>(_connectParams.pubkey,
|
||||||
|
_connectParams.secret,
|
||||||
|
new NIP47.NIP47Request("get_balance"), cancellation);
|
||||||
|
return new LightningNodeBalance()
|
||||||
|
{
|
||||||
|
OffchainBalance = new OffchainBalance()
|
||||||
|
{
|
||||||
|
Local = LightMoney.MilliSatoshis(response.BalanceMsats),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PayResponse> Pay(PayInvoiceParams payParams,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
return await Pay(null, new PayInvoiceParams(), cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PayResponse> 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<NIP47.Nip47Transaction>(_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<PayResponse> Pay(string bolt11, CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
return await Pay(bolt11, new PayInvoiceParams(), cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest,
|
||||||
|
CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BitcoinAddress> GetDepositAddress(CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ConnectionResult> 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<LightningChannel[]> ListChannels(CancellationToken cancellation = new CancellationToken())
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<NIP47.GetInfoResponse>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
|
||||||
|
@{
|
||||||
|
var storeId = Model.StoreId;
|
||||||
|
if (Model.CryptoCode != "BTC")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
|
const customNodeAccordian = document.getElementById("CustomNodeSupport");
|
||||||
|
const template = document.getElementById("nwc");
|
||||||
|
customNodeAccordian.appendChild(template.content.cloneNode(true));
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template id="nwc">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="CustomNWCHeader">
|
||||||
|
<button type="button" class="accordion-button collapsed" data-bs-toggle="collapse" data-bs-target="#CustomNWCContent" aria-controls="CustomNWCContent" aria-expanded="false">
|
||||||
|
<span><strong>Nostr Wallet Connect</strong> via NIP47</span>
|
||||||
|
<vc:icon symbol="caret-down"/>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="CustomNWCContent" class="accordion-collapse collapse" aria-labelledby="CustomNWCHeader" data-bs-parent="#CustomNodeSupport">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<ul class="pb-2">
|
||||||
|
<li>
|
||||||
|
<code><b>type=</b>nwc;<b>key=</b>b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code><b>nostr+walletconnect:</b>b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NNostr.Client" Version="0.0.40"></PackageReference>
|
<PackageReference Include="NNostr.Client" Version="0.0.43"></PackageReference>
|
||||||
<PackageReference Include="WabiSabi" Version="1.0.1.2"/>
|
<PackageReference Include="WabiSabi" Version="1.0.1.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Target Name="DeleteExampleFile" AfterTargets="Publish">
|
<Target Name="DeleteExampleFile" AfterTargets="Publish">
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class Nostr
|
|||||||
{
|
{
|
||||||
new() {TagIdentifier = EndpointTagIdentifier, Data = new List<string>() {new Uri(coordinatorUri, "plugins/wabisabi-coordinator/").ToString()}},
|
new() {TagIdentifier = EndpointTagIdentifier, Data = new List<string>() {new Uri(coordinatorUri, "plugins/wabisabi-coordinator/").ToString()}},
|
||||||
new() {TagIdentifier = TypeTagIdentifier, Data = new List<string>() { TypeTagValue}},
|
new() {TagIdentifier = TypeTagIdentifier, Data = new List<string>() { TypeTagValue}},
|
||||||
new() {TagIdentifier = NetworkTagIdentifier, Data = new List<string>() {currentNetwork.Name.ToLower()}}
|
new() {TagIdentifier = NetworkTagIdentifier, Data = new List<string>() {currentNetwork.ChainName.ToString().ToLower()}}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,12 +83,13 @@ public class Nostr
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
var result = new List<NostrEvent>();
|
var result = new List<NostrEvent>();
|
||||||
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);
|
cancellationToken);
|
||||||
await nostrClient.Connect(cts.Token);
|
await nostrClient.Connect(cts.Token);
|
||||||
|
|
||||||
|
|
||||||
result = await nostrClient.SubscribeForEvents(
|
result = await nostrClient.SubscribeForEvents(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
@@ -98,7 +99,7 @@ public class Nostr
|
|||||||
ExtensionData = new Dictionary<string, JsonElement>()
|
ExtensionData = new Dictionary<string, JsonElement>()
|
||||||
{
|
{
|
||||||
["#type"] = JsonSerializer.SerializeToElement(new[] {TypeTagValue}),
|
["#type"] = JsonSerializer.SerializeToElement(new[] {TypeTagValue}),
|
||||||
["#network"] = JsonSerializer.SerializeToElement(new[] {network})
|
["#network"] = JsonSerializer.SerializeToElement(new[] {network, currentNetwork.Name.ToLower()})
|
||||||
},
|
},
|
||||||
Limit = 1000
|
Limit = 1000
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule submodules/btcpayserver updated: 4ebe46830b...3fbc717cd4
Reference in New Issue
Block a user