mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 23:54:26 +01:00
add nwc and bump nostr
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
<PropertyGroup>
|
||||
<Product>Nostr </Product>
|
||||
<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>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
@@ -36,7 +36,7 @@
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.38" />
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.43" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources" />
|
||||
|
||||
@@ -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<IUIExtension>(new UIExtension("Nip05Nav",
|
||||
"store-integrations-nav"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NWC/LNPaymentMethodSetupTab", "ln-payment-method-setup-tab"));
|
||||
|
||||
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlDescriptionFilter>();
|
||||
applicationBuilder.AddSingleton<IPluginHookFilter, LnurlFilter>();
|
||||
applicationBuilder.AddSingleton<Zapper>();
|
||||
applicationBuilder.AddHostedService(sp => sp.GetRequiredService<Zapper>());
|
||||
applicationBuilder.AddSingleton<NostrWalletConnectLightningConnectionStringHandler>();
|
||||
applicationBuilder.AddSingleton<ILightningConnectionStringHandler>(provider => provider.GetRequiredService<NostrWalletConnectLightningConnectionStringHandler>());
|
||||
|
||||
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>
|
||||
</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"/>
|
||||
</ItemGroup>
|
||||
<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 = 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 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<string, JsonElement>()
|
||||
{
|
||||
["#type"] = JsonSerializer.SerializeToElement(new[] {TypeTagValue}),
|
||||
["#network"] = JsonSerializer.SerializeToElement(new[] {network})
|
||||
["#network"] = JsonSerializer.SerializeToElement(new[] {network, currentNetwork.Name.ToLower()})
|
||||
},
|
||||
Limit = 1000
|
||||
}
|
||||
|
||||
Submodule submodules/btcpayserver updated: 4ebe46830b...3fbc717cd4
Reference in New Issue
Block a user