From 95bef5a7c4f48675a21ed1885a556d7b412468df Mon Sep 17 00:00:00 2001 From: rockstardev <5191402+rockstardev@users.noreply.github.com> Date: Mon, 12 May 2025 23:42:21 -0500 Subject: [PATCH] Formatting code to align with .editorconfig --- .../Payments/Lightning/LightningExtensions.cs | 7 +- .../Lightning/LightningLikePaymentHandler.cs | 32 +++-- .../Payments/Lightning/LightningListener.cs | 117 +++++++++++------- 3 files changed, 100 insertions(+), 56 deletions(-) diff --git a/BTCPayServer/Payments/Lightning/LightningExtensions.cs b/BTCPayServer/Payments/Lightning/LightningExtensions.cs index bb3a62a4d..94010aa4f 100644 --- a/BTCPayServer/Payments/Lightning/LightningExtensions.cs +++ b/BTCPayServer/Payments/Lightning/LightningExtensions.cs @@ -10,9 +10,12 @@ namespace BTCPayServer.Payments.Lightning { public static bool IsConfigured(this LightningPaymentMethodConfig supportedPaymentMethod, BTCPayNetwork network, LightningNetworkOptions options) { - return supportedPaymentMethod.GetExternalLightningUrl() is not null || (supportedPaymentMethod.IsInternalNode && options.InternalLightningByCryptoCode.ContainsKey(network.CryptoCode)); + return supportedPaymentMethod.GetExternalLightningUrl() is not null || + (supportedPaymentMethod.IsInternalNode && options.InternalLightningByCryptoCode.ContainsKey(network.CryptoCode)); } - public static ILightningClient CreateLightningClient(this LightningPaymentMethodConfig supportedPaymentMethod, BTCPayNetwork network, LightningNetworkOptions options, LightningClientFactoryService lightningClientFactory) + + public static ILightningClient CreateLightningClient(this LightningPaymentMethodConfig supportedPaymentMethod, BTCPayNetwork network, + LightningNetworkOptions options, LightningClientFactoryService lightningClientFactory) { var external = supportedPaymentMethod.GetExternalLightningUrl(); if (external != null) diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 659f64709..5759a115a 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Payments.Lightning { new LightningPaymentData ParsePaymentDetails(JToken details); } + public class LightningLikePaymentHandler : IPaymentMethodHandler, ILightningPaymentHandler { public JsonSerializer Serializer { get; } @@ -73,6 +74,7 @@ namespace BTCPayServer.Payments.Lightning public BTCPayNetwork Network => _Network; static LightMoney OneSat = LightMoney.FromUnit(1.0m, LightMoneyUnit.Satoshi); + public async Task ConfigurePrompt(PaymentMethodContext context) { if (context.InvoiceEntity.Type == InvoiceType.TopUp) @@ -101,8 +103,8 @@ namespace BTCPayServer.Payments.Lightning string description = storeBlob.LightningDescriptionTemplate; description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); + .Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); using (var cts = new CancellationTokenSource(LightningTimeout)) { try @@ -139,7 +141,8 @@ namespace BTCPayServer.Payments.Lightning } - public async Task GetNodeInfo(LightningPaymentMethodConfig supportedPaymentMethod, PrefixedInvoiceLogs? invoiceLogs, bool? preferOnion = null, bool throws = false) + public async Task GetNodeInfo(LightningPaymentMethodConfig supportedPaymentMethod, PrefixedInvoiceLogs? invoiceLogs, + bool? preferOnion = null, bool throws = false) { var synced = _Dashboard.IsFullySynched(_Network.CryptoCode, out var summary); if (supportedPaymentMethod.IsInternalNode && !synced) @@ -193,6 +196,7 @@ namespace BTCPayServer.Payments.Lightning $"The lightning node is not synched ({blocksGap} blocks left)"); } } + return nodeInfo; } catch (Exception e) when (!throws) @@ -222,30 +226,37 @@ namespace BTCPayServer.Payments.Lightning throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})"); } } + public LightningPaymentMethodConfig ParsePaymentMethodConfig(JToken config) { return config.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(LightningPaymentMethodConfig)}"); } + object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config) { return ParsePaymentMethodConfig(config); } + object IPaymentMethodHandler.ParsePaymentPromptDetails(JToken details) { return ParsePaymentPromptDetails(details); } + public LigthningPaymentPromptDetails ParsePaymentPromptDetails(JToken details) { return details.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(LigthningPaymentPromptDetails)}"); } + public LightningPaymentData ParsePaymentDetails(JToken details) { return details.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(LightningPaymentData)}"); } + object IPaymentMethodHandler.ParsePaymentDetails(JToken details) { return ParsePaymentDetails(details); } + public async Task ValidatePaymentMethodConfig(PaymentMethodConfigValidationContext validationContext) { if (validationContext.Config is JValue { Type: JTokenType.String }) @@ -273,13 +284,15 @@ namespace BTCPayServer.Payments.Lightning return; } } + if (!client.IsSafe(config.ConnectionString)) { var canManage = (await validationContext.AuthorizationService.AuthorizeAsync(validationContext.User, null, - new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded; + new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded; if (!canManage) { - validationContext.ModelState.AddModelError(nameof(config.ConnectionString), $"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'."); + validationContext.ModelState.AddModelError(nameof(config.ConnectionString), + $"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'."); return; } } @@ -299,11 +312,13 @@ namespace BTCPayServer.Payments.Lightning if (oldConfig?.IsInternalNode != config.IsInternalNode && config.IsInternalNode) { var canUseInternalNode = _policies.Settings.AllowLightningInternalNodeForAll || - (await validationContext.AuthorizationService.AuthorizeAsync(validationContext.User, null, - new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.ContainsKey(_Network.CryptoCode); + (await validationContext.AuthorizationService.AuthorizeAsync(validationContext.User, null, + new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded && + _lightningNetworkOptions.Value.InternalLightningByCryptoCode.ContainsKey(_Network.CryptoCode); if (!canUseInternalNode) { - validationContext.SetMissingPermission(Policies.CanUseInternalLightningNode, $"You are not authorized to use the internal lightning node. Either add '{Policies.CanUseInternalLightningNode}' to an API Key, or allow non-admin users to use the internal lightning node in the server settings."); + validationContext.SetMissingPermission(Policies.CanUseInternalLightningNode, + $"You are not authorized to use the internal lightning node. Either add '{Policies.CanUseInternalLightningNode}' to an API Key, or allow non-admin users to use the internal lightning node in the server settings."); return; } } @@ -313,6 +328,7 @@ namespace BTCPayServer.Payments.Lightning validationContext.ModelState.AddModelError(nameof(config.ConnectionString), "The connection string or setting the internal node is required"); return; } + validationContext.Config = JToken.FromObject(config, Serializer); #pragma warning restore CS0618 // Type or member is obsolete } diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index 3b6c70b87..5c65b4ad3 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -40,18 +40,18 @@ namespace BTCPayServer.Payments.Lightning private readonly PaymentMethodHandlerDictionary _handlers; readonly Channel _CheckInvoices = Channel.CreateUnbounded(); Task? _CheckingInvoice; - readonly Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>(); + readonly Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new(); public LightningListener(EventAggregator aggregator, - InvoiceRepository invoiceRepository, - IMemoryCache memoryCache, - BTCPayNetworkProvider networkProvider, - LightningClientFactoryService lightningClientFactory, - StoreRepository storeRepository, - IOptions options, - PaymentService paymentService, - PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, - Logs logs) + InvoiceRepository invoiceRepository, + IMemoryCache memoryCache, + BTCPayNetworkProvider networkProvider, + LightningClientFactoryService lightningClientFactory, + StoreRepository storeRepository, + IOptions options, + PaymentService paymentService, + PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, + Logs logs) { Logs = logs; _Aggregator = aggregator; @@ -66,12 +66,13 @@ namespace BTCPayServer.Payments.Lightning } bool needCheckOfflinePayments = true; + async Task CheckingInvoice(CancellationToken cancellation) { var pmis = _handlers.Where(h => h is LightningLikePaymentHandler).Select(handler => handler.PaymentMethodId).ToArray(); foreach (var pmi in pmis) { -retry: + retry: try { Logs.PayServer.LogInformation("Checking if any payment arrived on lightning while the server was offline..."); @@ -83,13 +84,13 @@ retry: _memoryCache.Set(GetCacheKey(invoice.Id), invoice, GetExpiration(invoice)); } } + needCheckOfflinePayments = false; Logs.PayServer.LogInformation("Processing lightning payments..."); - while (await _CheckInvoices.Reader.WaitToReadAsync(cancellation) && - _CheckInvoices.Reader.TryRead(out var invoiceId)) + _CheckInvoices.Reader.TryRead(out var invoiceId)) { var invoice = await GetInvoice(invoiceId); @@ -107,9 +108,11 @@ retry: { if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener)) { - instanceListener ??= new LightningInstanceListener(_InvoiceRepository, _Aggregator, lightningClientFactory, listenedInvoice.Network, _handlers, connStr, _paymentService, Logs); + instanceListener ??= new LightningInstanceListener(_InvoiceRepository, _Aggregator, lightningClientFactory, + listenedInvoice.Network, _handlers, connStr, _paymentService, Logs); _InstanceListeners.TryAdd(instanceListenerKey, instanceListener); } + instanceListener.AddListenedInvoice(listenedInvoice); _ = instanceListener.PollPayment(listenedInvoice, cancellation); } @@ -135,6 +138,7 @@ retry: { return $"{nameof(GetListenedInvoices)}-{invoiceId}"; } + private Task GetInvoice(string invoiceId) { return _memoryCache.GetOrCreateAsync(GetCacheKey(invoiceId), async (cacheEntry) => @@ -176,6 +180,7 @@ retry: yield return (handler, prompt, handler.ParsePaymentPromptDetails(prompt.Details)); } } + private List GetListenedInvoices(InvoiceEntity invoice) { var listenedInvoices = new List(); @@ -190,11 +195,13 @@ retry: ((IHasNetwork)o.Handler).Network, invoice.Id)); } + return listenedInvoices; } readonly ConcurrentDictionary _ListeningInstances = new ConcurrentDictionary(); readonly CompositeDisposable leases = new CompositeDisposable(); + public Task StartAsync(CancellationToken cancellationToken) { leases.Add(_Aggregator.SubscribeAsync(async inv => @@ -204,7 +211,8 @@ retry: _CheckInvoices.Writer.TryWrite(inv.Invoice.Id); } - if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatus.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) + if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatus.New && + inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) { var pm = inv.Invoice.GetPaymentPrompts().First(); if (pm.Calculate().Due > 0m) @@ -221,7 +229,6 @@ retry: var invoice = await _InvoiceRepository.GetInvoice(inv.InvoiceId); await CreateNewLNInvoiceForBTCPayInvoice(invoice); } - })); leases.Add(_Aggregator.Subscribe(inv => { @@ -249,7 +256,6 @@ retry: CheckConnections(); } catch { } - }, null, 0, (int)PollInterval.TotalMilliseconds); leases.Add(_ListenPoller); return Task.CompletedTask; @@ -318,10 +324,10 @@ retry: { //not a fully supported option } + lnurlPayPaymentMethodDetails = new LNURLPayPaymentMethodDetails() { - Bech32Mode = lnurlPayPaymentMethodDetails.Bech32Mode, - NodeInfo = lnurlPayPaymentMethodDetails.NodeInfo, + Bech32Mode = lnurlPayPaymentMethodDetails.Bech32Mode, NodeInfo = lnurlPayPaymentMethodDetails.NodeInfo, }; o.PaymentPrompt.Destination = null; @@ -344,7 +350,8 @@ retry: //not a fully supported option } - var paymentContext = new PaymentMethodContext(store, store.GetStoreBlob(), JToken.FromObject(lnConfig, _handlers.GetLightningHandler(network).Serializer), lightningHandler, invoice, logs); + var paymentContext = new PaymentMethodContext(store, store.GetStoreBlob(), + JToken.FromObject(lnConfig, _handlers.GetLightningHandler(network).Serializer), lightningHandler, invoice, logs); var paymentPrompt = paymentContext.Prompt; await paymentContext.BeforeFetchingRates(); await paymentContext.CreatePaymentPrompt(); @@ -356,6 +363,7 @@ retry: { _InstanceListeners.TryGetValue(instanceListenerKey, out instanceListener); } + if (instanceListener is not null) { await _InvoiceRepository.NewPaymentPrompt(invoice.Id, paymentContext); @@ -377,10 +385,10 @@ retry: InvoiceEventData.EventSeverity.Error); } } + await _InvoiceRepository.AddInvoiceLogs(invoice.Id, logs); _CheckInvoices.Writer.TryWrite(invoice.Id); } - } private string? GetLightningUrl(string cryptoCode, LightningPaymentMethodConfig supportedMethod) @@ -392,6 +400,7 @@ retry: } TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0); + public TimeSpan PollInterval { get @@ -407,6 +416,7 @@ retry: } } } + private Timer? _ListenPoller; public IOptions Options { get; } @@ -424,16 +434,16 @@ retry: } catch (OperationCanceledException) { - } + try { await Task.WhenAll(_ListeningInstances.Select(c => c.Value.Listening).Where(c => c != null).ToArray()!); } catch (OperationCanceledException) { - } + Logs.PayServer.LogInformation($"{this.GetType().Name} successfully exited..."); } } @@ -453,13 +463,13 @@ retry: public string ConnectionString { get; } public LightningInstanceListener(InvoiceRepository invoiceRepository, - EventAggregator eventAggregator, - LightningClientFactoryService lightningClientFactory, - BTCPayNetwork network, - PaymentMethodHandlerDictionary handlers, - string connectionString, - PaymentService paymentService, - Logs logs) + EventAggregator eventAggregator, + LightningClientFactoryService lightningClientFactory, + BTCPayNetwork network, + PaymentMethodHandlerDictionary handlers, + string connectionString, + PaymentService paymentService, + Logs logs) { ArgumentNullException.ThrowIfNull(connectionString); Logs = logs; @@ -471,6 +481,7 @@ retry: _lightningClientFactory = lightningClientFactory; ConnectionString = connectionString; } + internal bool AddListenedInvoice(ListenedInvoice invoice) { return _ListenedInvoices.TryAdd(invoice.PaymentMethodDetails.InvoiceId, invoice); @@ -485,12 +496,14 @@ retry: { Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}"); } + return lightningInvoice?.Status; } public bool Empty => _ListenedInvoices.IsEmpty; public bool IsListening => Listening?.Status is TaskStatus.Running || Listening?.Status is TaskStatus.WaitingForActivation; public Task? Listening { get; set; } + public void EnsureListening(CancellationToken cancellation) { if (!IsListening) @@ -501,14 +514,16 @@ retry: Listening = Listen(StopListeningCancellationTokenSource.Token); } } + public CancellationTokenSource? StopListeningCancellationTokenSource; + async Task Listen(CancellationToken cancellation) { string? uri = null; try { var lightningClient = _lightningClientFactory.Create(ConnectionString, _network); - if(lightningClient is null) + if (lightningClient is null) return; uri = lightningClient.GetServerUri(ConnectionString)?.RemoveUserInfo() ?? ""; Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Start listening {Uri}", _network.CryptoCode, uri); @@ -519,6 +534,7 @@ retry: { Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Could reconnect successfully to {Uri}", _network.CryptoCode, uri); } + _ErrorAlreadyLogged = false; while (!_ListenedInvoices.IsEmpty) { @@ -534,8 +550,10 @@ retry: { if (await AddPayment(notification, listenedInvoice.InvoiceId, listenedInvoice.PaymentMethod.PaymentMethodId)) { - Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Payment detected via notification ({InvoiceId})", _network.CryptoCode, listenedInvoice.InvoiceId); + Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Payment detected via notification ({InvoiceId})", _network.CryptoCode, + listenedInvoice.InvoiceId); } + _ListenedInvoices.TryRemove(notification.Id, out var _); } else if (notification.Status == LightningInvoiceStatus.Expired) @@ -552,8 +570,10 @@ retry: Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Stop listening {Uri}", _network.CryptoCode, uri); } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { } + if (_ListenedInvoices.IsEmpty) - Logs.PayServer.LogInformation("{CryptoCode} (Lightning): No more invoice to listen on {Uri}, releasing the connection", _network.CryptoCode, uri); + Logs.PayServer.LogInformation("{CryptoCode} (Lightning): No more invoice to listen on {Uri}, releasing the connection", _network.CryptoCode, + uri); } private uint256? GetPaymentHash(ListenedInvoice listenedInvoice) @@ -570,10 +590,11 @@ retry: { var status = await PollPayment(invoice, cancellation); if (status is null || - status is LightningInvoiceStatus.Paid || - status is LightningInvoiceStatus.Expired) + status is LightningInvoiceStatus.Paid || + status is LightningInvoiceStatus.Expired) _ListenedInvoices.TryRemove(invoice.PaymentMethodDetails.InvoiceId, out var _); } + LastFullPoll = DateTimeOffset.UtcNow; if (_ListenedInvoices.IsEmpty) { @@ -600,28 +621,31 @@ retry: Currency = _network.CryptoCode, InvoiceDataId = invoiceId, Amount = (notification.AmountReceived ?? notification.Amount).ToDecimal(LightMoneyUnit.BTC), - }.Set(invoiceEntity, handler, new LightningLikePaymentData() - { - PaymentHash = paymentHash, - Preimage = string.IsNullOrEmpty(notification.Preimage) ? null : uint256.Parse(notification.Preimage), - }); + }.Set(invoiceEntity, handler, + new LightningLikePaymentData() + { + PaymentHash = paymentHash, Preimage = string.IsNullOrEmpty(notification.Preimage) ? null : uint256.Parse(notification.Preimage), + }); var payment = await _paymentService.AddPayment(paymentData, [notification.BOLT11]); if (payment != null) { if (notification.Preimage is not null) { - var details = (LigthningPaymentPromptDetails)handler.ParsePaymentPromptDetails(invoiceEntity.GetPaymentPrompt(handler.PaymentMethodId)!.Details); + var details = (LigthningPaymentPromptDetails)handler.ParsePaymentPromptDetails(invoiceEntity.GetPaymentPrompt(handler.PaymentMethodId)! + .Details); if (details.Preimage is null) { details.Preimage = uint256.Parse(notification.Preimage); await _invoiceRepository.UpdatePaymentDetails(invoiceId, handler, details); } } + var invoice = await _invoiceRepository.GetInvoice(invoiceId); if (invoice != null) _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); } + return payment != null; } @@ -632,17 +656,18 @@ retry: if (invoice.Value.IsExpired()) _ListenedInvoices.TryRemove(invoice.Key, out var _); } + if (_ListenedInvoices.IsEmpty) StopListeningCancellationTokenSource?.Cancel(); } } public record ListenedInvoice( - DateTimeOffset Expiration, - LigthningPaymentPromptDetails PaymentMethodDetails, - PaymentPrompt PaymentMethod, - BTCPayNetwork Network, - string InvoiceId) + DateTimeOffset Expiration, + LigthningPaymentPromptDetails PaymentMethodDetails, + PaymentPrompt PaymentMethod, + BTCPayNetwork Network, + string InvoiceId) { public bool IsExpired() { return DateTimeOffset.UtcNow > Expiration; } }