From 08732a64c1e857d68e08bd6cde8d981db7210178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:42:42 +0200 Subject: [PATCH] Increase channel view performance when loading (#246) * refactor(NodeChannelSubscribeJob.cs): remove check for empty close address to allow for channels without a close address style(Channels.razor): add space after typecast for consistency and readability refactor(Channels.razor): simplify balance calculation logic by removing unnecessary null check and conversion feat(Channels.razor): add _channelsBalance dictionary to store channel balances for efficiency feat(Channels.razor): add balance update every 60 seconds to keep channel balances up-to-date fix(Channels.razor): change return type of GetPercentageBalance to int and handle exceptions more gracefully style(Channels.razor): adjust indentation for better code readability style(Channels.razor): add space after typecast for consistency and readability style(Channels.razor): adjust line breaks for better code readability refactor(LightningService.cs): improve code readability by breaking down long lines of code feat(LightningService.cs): modify GetChannelBalance method to return a dictionary of balances for all channels fix(LightningService.cs): improve error handling and logging for better debugging and error tracking style(LightningService.cs): improve code readability by breaking long lines into multiple lines This commit breaks down long lines of code into multiple lines to improve readability and maintainability. This includes error messages, function calls, and conditional statements. No functional changes were made. style(LightningService.cs): improve code readability by breaking long lines into multiple lines This commit breaks down long lines of code into multiple lines to improve readability and maintainability. This is particularly useful for developers who use tools or editors with limited horizontal space. It also makes it easier to understand the structure and flow of the code at a glance. refactor(LightningService.cs): change GetChannelBalance method to GetChannelsBalance to fetch balances for all channels feat(LightningService.cs): add logic to calculate local and remote balances for each channel in GetChannelsBalance method to provide more detailed balance information * Update src/Pages/Channels.razor Co-authored-by: Marcos <33052423+markettes@users.noreply.github.com> --------- Co-authored-by: Marcos <33052423+markettes@users.noreply.github.com> --- src/Jobs/NodeChannelSubscribeJob.cs | 5 - src/Pages/Channels.razor | 67 ++++--- src/Services/LightningService.cs | 300 ++++++++++++++++++---------- 3 files changed, 239 insertions(+), 133 deletions(-) diff --git a/src/Jobs/NodeChannelSubscribeJob.cs b/src/Jobs/NodeChannelSubscribeJob.cs index 07d7cd91..c09b4c12 100644 --- a/src/Jobs/NodeChannelSubscribeJob.cs +++ b/src/Jobs/NodeChannelSubscribeJob.cs @@ -109,11 +109,6 @@ public async Task NodeUpdateManagement(ChannelEventUpdate channelEventUpdate, No switch (channelEventUpdate.Type) { case ChannelEventUpdate.Types.UpdateType.OpenChannel: - if (String.IsNullOrEmpty(channelEventUpdate.OpenChannel.CloseAddress)) - { - _logger.LogError("Close address is empty"); - throw new Exception("Close address is empty"); - } var channelOpened = channelEventUpdate.OpenChannel; var fundingTxAndIndex = channelOpened.ChannelPoint.Split(":"); diff --git a/src/Pages/Channels.razor b/src/Pages/Channels.razor index f99abdcc..7969f92d 100644 --- a/src/Pages/Channels.razor +++ b/src/Pages/Channels.razor @@ -107,7 +107,7 @@ Wallet wallet = null; if (context.OpenedWithId != null) { - wallet = Task.Run(() => WalletRepository.GetById((int)context.OpenedWithId)).Result; + wallet = Task.Run(() => WalletRepository.GetById((int) context.OpenedWithId)).Result; } @(wallet == null ? "Unknown" : wallet.Name) } @@ -148,16 +148,11 @@ @{ var balance = Task.Run(() => GetPercentageBalance(context)).Result; - var percentageBalance = 0; - if (balance != null) - { - percentageBalance = Convert.ToInt32(balance); - } - + } - @if (balance != null) + @if (balance >= 0) { - + @{ @($"{balance}%") @@ -365,6 +360,9 @@ private ColumnLayout ChannelsColumnLayout; private Dictionary ChannelsColumns = new(); private bool columnsLoaded; + // This dictionary is used to store the balance of each channel, key is the channel id, value is a tuple with the node id as local in the pair, local balance, remote balance + private Dictionary _channelsBalance = new(); + private DateTimeOffset _lastBalanceUpdate = DateTimeOffset.Now; public abstract class ChannelsColumnName { @@ -406,6 +404,7 @@ _nodes = await NodeRepository.GetAll(); _wallets = await WalletRepository.GetAll(); _channelsDataGridRef.FilterData(); + _channelsBalance = await LightningService.GetChannelsBalance(); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -413,7 +412,6 @@ if (!firstRender && !columnsLoaded) { await LoadColumnLayout(); - } } @@ -469,28 +467,42 @@ ToastService.ShowSuccess("Text copied"); } - private async Task GetPercentageBalance(Channel channel) + private async Task GetPercentageBalance(Channel channel) { + //If the last update was more than 60 seconds ago, we update the balance dictionary + if (DateTimeOffset.Now.Subtract(_lastBalanceUpdate).TotalSeconds > 60) + { + _channelsBalance = await LightningService.GetChannelsBalance(); + _lastBalanceUpdate = DateTimeOffset.Now; + } + try { - double? result = null; - var values = await LightningService.GetChannelBalance(channel); - if (values.Item1 != null && values.Item2 != null) - { - result = (values.Item2 / (double) (values.Item2 + values.Item1)) * 100; - result = Math.Round(result.Value, 2); - } - else + var result = -1.0; + if (_channelsBalance.TryGetValue(channel.ChanId, out var values)) { - result = null; + var sourceNodeId = values.Item1; + //If the source node is the local node, the remote balance is the third value, otherwise is the second + var capacity = values.Item2 + values.Item3; + if (sourceNodeId == channel.SourceNodeId) + { + result = (values.Item3 / (double) capacity) * 100; + } + else + { + result = (values.Item2 / (double) capacity) * 100; + } + + result = Math.Round(result, 2); } - return result; + + return Convert.ToInt32(result); } catch (Exception e) { ToastService.ShowError($"Channel balance for channel id:{channel.Id} could not be retrieved"); - return null; + return -1; } } @@ -574,7 +586,7 @@ var sourceNode = await NodeRepository.GetById(channel.SourceNodeId); var node = String.IsNullOrEmpty(sourceNode.ChannelAdminMacaroon) ? destinationNode : sourceNode; - //If there is a liquidity rule for this channel, we load it, the first one + //If there is a liquidity rule for this channel, we load it, the first one _currentLiquidityRule = _selectedChannel?.LiquidityRules.FirstOrDefault() ?? new LiquidityRule { MinimumLocalBalance = 20, @@ -718,7 +730,7 @@ private bool OnWalletFilter(object itemValue, object searchValue) { if (itemValue == null) return false; - return searchValue == null || (int)searchValue == 0 || (int)itemValue == (int)searchValue; + return searchValue == null || (int) searchValue == 0 || (int) itemValue == (int) searchValue; } private bool OnSourceNodeIdFilter(object itemValue, object searchValue) @@ -766,9 +778,9 @@ { ChannelOperationRequest? lastRequest = channel.ChannelOperationRequests.LastOrDefault(); if (lastRequest == null && channel.CreatedByNodeGuard == false) return false; - return channel.Status == Channel.ChannelStatus.Closed|| (lastRequest.RequestType == OperationRequestType.Close - && lastRequest.Status == ChannelOperationRequestStatus.OnChainConfirmed - || lastRequest.Status == ChannelOperationRequestStatus.OnChainConfirmationPending); + return channel.Status == Channel.ChannelStatus.Closed || (lastRequest.RequestType == OperationRequestType.Close + && lastRequest.Status == ChannelOperationRequestStatus.OnChainConfirmed + || lastRequest.Status == ChannelOperationRequestStatus.OnChainConfirmationPending); } private void OnColumnLayoutUpdate() @@ -784,4 +796,5 @@ } return ChannelsColumnLayout.IsColumnVisible(column); } + } \ No newline at end of file diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index 17d23a45..5c8a125a 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -95,11 +95,10 @@ public interface ILightningService public Task GetNodeInfo(string pubkey); /// - /// Channel balance + /// Gets a dictionary of the local and remote balance of all the channels managed by NG /// - /// /// - public Task<(long?, long?)> GetChannelBalance(Channel channel); + public Task> GetChannelsBalance(); /// /// Cancels a pending channel from LND PSBT-based funding of channels @@ -107,7 +106,8 @@ public interface ILightningService /// /// /// - public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockable? client = null); + public void CancelPendingChannel(Node source, byte[] pendingChannelId, + IUnmockable? client = null); } public class LightningService : ILightningService @@ -177,7 +177,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) var network = CurrentNetworkHelper.GetCurrentNetwork(); - _logger.LogInformation("Channel open request for request id: {RequestId} from node: {SourceNodeName} to node: {DestinationNodeName}", + _logger.LogInformation( + "Channel open request for request id: {RequestId} from node: {SourceNodeName} to node: {DestinationNodeName}", channelOperationRequest.Id, source.Name, destination.Name); @@ -198,16 +199,22 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) !x.IsTemplatePSBT); //If it is a hot wallet, we dont check the number of (human) signatures - if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN - 1) + if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && + channelOperationRequest.Wallet != null && + humanSignaturesCount != channelOperationRequest.Wallet.MofN - 1) { - _logger.LogError("The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", channelOperationRequest.Wallet.MofN - 1, humanSignaturesCount); - throw new InvalidOperationException("The number of human signatures does not match the number of signatures required for this wallet"); + _logger.LogError( + "The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", + channelOperationRequest.Wallet.MofN - 1, humanSignaturesCount); + throw new InvalidOperationException( + "The number of human signatures does not match the number of signatures required for this wallet"); } if (channelOperationRequest.Changeless && combinedPSBT.Outputs.Any()) { _logger.LogError("Changeless channel operation request cannot have outputs at this stage"); - throw new InvalidOperationException("Changeless channel operation request cannot have outputs at this stage"); + throw new InvalidOperationException( + "Changeless channel operation request cannot have outputs at this stage"); } //Prior to opening the channel, we add the remote node as a peer @@ -219,12 +226,15 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) throw new InvalidOperationException(); } - var initialFeeRate = channelOperationRequest.FeeRate ?? (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate.SatoshiPerByte; + var initialFeeRate = channelOperationRequest.FeeRate ?? + (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate + .SatoshiPerByte; ; var fundingAmount = GetFundingAmount(channelOperationRequest, combinedPSBT, initialFeeRate); - var openChannelRequest = await CreateOpenChannelRequest(channelOperationRequest, combinedPSBT, remoteNodeInfo, fundingAmount, pendingChannelId, derivationStrategyBase); + var openChannelRequest = await CreateOpenChannelRequest(channelOperationRequest, combinedPSBT, + remoteNodeInfo, fundingAmount, pendingChannelId, derivationStrategyBase); //For now, we only rely on pure tcp IPV4 connections var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp")?.Addr; @@ -243,11 +253,11 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { connectPeerResponse = await client.Execute(x => x.ConnectPeerAsync(new ConnectPeerRequest { - Addr = new LightningAddress { Host = addr, Pubkey = remoteNodeInfo.PubKey }, + Addr = new LightningAddress {Host = addr, Pubkey = remoteNodeInfo.PubKey}, Perm = true }, new Metadata { - { "macaroon", source.ChannelAdminMacaroon } + {"macaroon", source.ChannelAdminMacaroon} }, null, default)); } //We avoid to stop the method if the peer is already connected @@ -290,7 +300,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) if (source.ChannelAdminMacaroon != null) { var openStatusUpdateStream = client.Execute(x => x.OpenChannel(openChannelRequest, - new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default + new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, default )); await foreach (var response in openStatusUpdateStream.ResponseStream.ReadAllAsync()) @@ -307,7 +317,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) "Channel pending for channel operation request id: {RequestId} for pending channel id: {ChannelId}", channelOperationRequest.Id, pendingChannelIdHex); - channelOperationRequest.Status = ChannelOperationRequestStatus.OnChainConfirmationPending; + channelOperationRequest.Status = + ChannelOperationRequestStatus.OnChainConfirmationPending; channelOperationRequest.TxId = LightningHelper.DecodeTxId(response.ChanPending.Txid); _channelOperationRequestRepository.Update(channelOperationRequest); @@ -321,17 +332,20 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) channelOperationRequest.Status = ChannelOperationRequestStatus.OnChainConfirmed; if (channelOperationRequest.StatusLogs.Count > 0) { - channelOperationRequest.StatusLogs.Add(ChannelStatusLog.Info($"Channel opened successfully 🎉")); + channelOperationRequest.StatusLogs.Add( + ChannelStatusLog.Info($"Channel opened successfully 🎉")); } _channelOperationRequestRepository.Update(channelOperationRequest); - var fundingTx = LightningHelper.DecodeTxId(response.ChanOpen.ChannelPoint.FundingTxidBytes); + var fundingTx = + LightningHelper.DecodeTxId(response.ChanOpen.ChannelPoint.FundingTxidBytes); //Get the channels to find the channelId, not the temporary one var channels = await client.Execute(x => x.ListChannelsAsync(new ListChannelsRequest(), - new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default)); - var currentChannel = channels.Channels.SingleOrDefault(x => x.ChannelPoint == $"{fundingTx}:{response.ChanOpen.ChannelPoint.OutputIndex}"); + new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, default)); + var currentChannel = channels.Channels.SingleOrDefault(x => + x.ChannelPoint == $"{fundingTx}:{response.ChanOpen.ChannelPoint.OutputIndex}"); if (currentChannel == null) { @@ -392,11 +406,14 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) case OpenStatusUpdate.UpdateOneofCase.PsbtFund: channelOperationRequest.Status = ChannelOperationRequestStatus.FinalizingPSBT; - var (isSuccess, error) = _channelOperationRequestRepository.Update(channelOperationRequest); + var (isSuccess, error) = + _channelOperationRequestRepository.Update(channelOperationRequest); if (!isSuccess) { - var errorMessage = string.Format("Request in funding stage, but could not update status to {Status} for request id: {RequestId} reason: {Reason}", - ChannelOperationRequestStatus.FinalizingPSBT, channelOperationRequest.Id, error); + var errorMessage = string.Format( + "Request in funding stage, but could not update status to {Status} for request id: {RequestId} reason: {Reason}", + ChannelOperationRequestStatus.FinalizingPSBT, channelOperationRequest.Id, + error); _logger.LogError(errorMessage); throw new Exception(errorMessage); } @@ -423,8 +440,12 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { var totalIn = fundedPSBT.Inputs.Sum(i => i.GetTxOut()?.Value); //We manually fix the change (it was wrong from the Base template due to nbitcoin requiring a change on a PSBT) - var totalChangefulFees = new Money(vsize * initialFeeRate, MoneyUnit.Satoshi); - var changeOutput = channelfundingTx.Outputs.SingleOrDefault(o => o.Value != channelOperationRequest.SatsAmount) ?? channelfundingTx.Outputs.First(); + var totalChangefulFees = + new Money(vsize * initialFeeRate, MoneyUnit.Satoshi); + var changeOutput = + channelfundingTx.Outputs.SingleOrDefault(o => + o.Value != channelOperationRequest.SatsAmount) ?? + channelfundingTx.Outputs.First(); changeOutput.Value = totalIn - totalOut - totalChangefulFees; //We merge changeFixedPSBT with the other PSBT with the change fixed @@ -432,7 +453,9 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) } else { - throw new ExternalException("VSized could not be calculated for the funded PSBT, channel operation request id: {RequestId}", channelOperationRequest.Id); + throw new ExternalException( + "VSized could not be calculated for the funded PSBT, channel operation request id: {RequestId}", + channelOperationRequest.Id); } } @@ -443,7 +466,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) finalSignedPSBT = await _remoteSignerService.Sign(fundedPSBT); if (finalSignedPSBT == null) { - const string errorMessage = "The signed PSBT was null, something went wrong while signing with the remote signer"; + const string errorMessage = + "The signed PSBT was null, something went wrong while signing with the remote signer"; _logger.LogError(errorMessage); throw new Exception( errorMessage); @@ -461,7 +485,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) if (finalSignedPSBT == null) { - const string errorMessage = "The signed PSBT was null, something went wrong while signing with the embedded signer"; + const string errorMessage = + "The signed PSBT was null, something went wrong while signing with the embedded signer"; _logger.LogError(errorMessage); throw new Exception( errorMessage); @@ -477,11 +502,15 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) IsInternalWalletPSBT = true }; - var addResult = await _channelOperationRequestPsbtRepository.AddAsync(signedChannelOperationRequestPsbt); + var addResult = + await _channelOperationRequestPsbtRepository.AddAsync( + signedChannelOperationRequestPsbt); if (!addResult.Item1) { - _logger.LogError("Could not store the signed PSBT for channel operation request id: {RequestId} reason: {Reason}", channelOperationRequest.Id, addResult.Item2); + _logger.LogError( + "Could not store the signed PSBT for channel operation request id: {RequestId} reason: {Reason}", + channelOperationRequest.Id, addResult.Item2); } //Time to finalize the PSBT and broadcast the tx @@ -494,7 +523,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) channelfundingTx = finalizedPSBT.ExtractTransaction(); //We check the feerate of the finalized PSBT by checking a minimum and maximum allowed and also a fee-level max check in ratio - var feerate = new FeeRate(finalizedPSBT.GetFee(), channelfundingTx.GetVirtualSize()); + var feerate = new FeeRate(finalizedPSBT.GetFee(), + channelfundingTx.GetVirtualSize()); var minFeeRate = Constants.MIN_SAT_PER_VB_RATIO * initialFeeRate; @@ -502,27 +532,39 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) if (feerate.SatoshiPerByte < minFeeRate) { - _logger.LogError("Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb} is lower than the minimum allowed: {MinSatPerVb}", channelOperationRequest.Id, feerate.SatoshiPerByte, minFeeRate); - throw new Exception("The finalized PSBT sat/vb is lower than the minimum allowed"); + _logger.LogError( + "Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb} is lower than the minimum allowed: {MinSatPerVb}", + channelOperationRequest.Id, feerate.SatoshiPerByte, minFeeRate); + throw new Exception( + "The finalized PSBT sat/vb is lower than the minimum allowed"); } if (feerate.SatoshiPerByte > maxFeeRate) { - _logger.LogError("Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb} is higher than the maximum allowed: {MaxSatPerVb}", channelOperationRequest.Id, feerate.SatoshiPerByte, maxFeeRate); - throw new Exception("The finalized PSBT sat/vb is higher than the maximum allowed"); + _logger.LogError( + "Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb} is higher than the maximum allowed: {MaxSatPerVb}", + channelOperationRequest.Id, feerate.SatoshiPerByte, maxFeeRate); + throw new Exception( + "The finalized PSBT sat/vb is higher than the maximum allowed"); } //if the fee is too high, we throw an exception - var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long)x.GetCoin()?.Amount); + var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long) x.GetCoin()?.Amount); if (finalizedPSBT.GetFee().Satoshi >= finalizedTotalIn * Constants.MAX_TX_FEE_RATIO) { - _logger.LogError("Channel operation request id: {RequestId} finalized PSBT fee: {Fee} is higher than the maximum allowed: {MaxFee} sats", channelOperationRequest.Id, finalizedPSBT.GetFee().Satoshi, finalizedTotalIn * Constants.MAX_TX_FEE_RATIO); - throw new Exception("The finalized PSBT fee is higher than the maximum allowed"); + _logger.LogError( + "Channel operation request id: {RequestId} finalized PSBT fee: {Fee} is higher than the maximum allowed: {MaxFee} sats", + channelOperationRequest.Id, finalizedPSBT.GetFee().Satoshi, + finalizedTotalIn * Constants.MAX_TX_FEE_RATIO); + throw new Exception( + "The finalized PSBT fee is higher than the maximum allowed"); } - _logger.LogInformation("Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb}", channelOperationRequest.Id, feerate.SatoshiPerByte); + _logger.LogInformation( + "Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb}", + channelOperationRequest.Id, feerate.SatoshiPerByte); //Just a check of the tx based on the finalizedPSBT var checkTx = channelfundingTx.Check(); @@ -536,15 +578,18 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) PsbtVerify = new FundingPsbtVerify { FundedPsbt = - ByteString.CopyFrom(Convert.FromHexString(finalizedPSBT.ToHex())), + ByteString.CopyFrom( + Convert.FromHexString(finalizedPSBT.ToHex())), PendingChanId = ByteString.CopyFrom(pendingChannelId) } - }, new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default)); + }, new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, + default)); //Saving the PSBT in the ChannelOperationRequest collection of PSBTs channelOperationRequest = - await _channelOperationRequestRepository.GetById(channelOperationRequest.Id) ?? throw new InvalidOperationException(); + await _channelOperationRequestRepository.GetById(channelOperationRequest + .Id) ?? throw new InvalidOperationException(); if (channelOperationRequest.ChannelOperationRequestPsbts != null) { @@ -557,7 +602,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) }; var finalisedPSBTAdd = await - _channelOperationRequestPsbtRepository.AddAsync(finalizedChannelOperationRequestPsbt); + _channelOperationRequestPsbtRepository.AddAsync( + finalizedChannelOperationRequestPsbt); if (!finalisedPSBTAdd.Item1) { @@ -575,19 +621,25 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) PendingChanId = ByteString.CopyFrom(pendingChannelId), //FinalRawTx = ByteString.CopyFrom(Convert.FromHexString(finalTxHex)), SignedPsbt = - ByteString.CopyFrom(Convert.FromHexString(finalizedPSBT.ToHex())) + ByteString.CopyFrom( + Convert.FromHexString(finalizedPSBT.ToHex())) }, - }, new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default)); + }, new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, + default)); } else { - _logger.LogError("TX Check failed for channel operation request id: {RequestId} reason: {Reason}", channelOperationRequest.Id, checkTx); + _logger.LogError( + "TX Check failed for channel operation request id: {RequestId} reason: {Reason}", + channelOperationRequest.Id, checkTx); CancelPendingChannel(source, pendingChannelId, client); } } else { - _logger.LogError("Could not parse the PSBT for funding channel operation request id: {RequestId}", channelOperationRequest.Id); + _logger.LogError( + "Could not parse the PSBT for funding channel operation request id: {RequestId}", + channelOperationRequest.Id); CancelPendingChannel(source, pendingChannelId, client); } @@ -639,12 +691,13 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) }; var grpcChannel = GrpcChannel.ForAddress($"https://{endpoint}", - new GrpcChannelOptions { HttpHandler = httpHandler }); + new GrpcChannelOptions {HttpHandler = httpHandler}); return new Lightning.LightningClient(grpcChannel).Wrap(); }; - public long GetFundingAmount(ChannelOperationRequest channelOperationRequest, PSBT combinedPSBT, decimal initialFeeRate) + public long GetFundingAmount(ChannelOperationRequest channelOperationRequest, PSBT combinedPSBT, + decimal initialFeeRate) { if (!combinedPSBT.TryGetVirtualSize(out var estimatedVsize)) { @@ -652,14 +705,21 @@ public long GetFundingAmount(ChannelOperationRequest channelOperationRequest, PS throw new InvalidOperationException("Could not estimate virtual size of the PSBT"); } - var changelessVSize = channelOperationRequest.Changeless ? 43 : 0; // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig) + var changelessVSize = + channelOperationRequest.Changeless + ? 43 + : 0; // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig) var outputVirtualSize = estimatedVsize + changelessVSize; // We add the change output if needed var totalFees = new Money(outputVirtualSize * initialFeeRate, MoneyUnit.Satoshi); - return channelOperationRequest.Changeless ? channelOperationRequest.SatsAmount - totalFees : channelOperationRequest.SatsAmount; + return channelOperationRequest.Changeless + ? channelOperationRequest.SatsAmount - totalFees + : channelOperationRequest.SatsAmount; } - public async Task CreateOpenChannelRequest(ChannelOperationRequest channelOperationRequest, PSBT? combinedPSBT, LightningNode? remoteNodeInfo, long fundingAmount, byte[] pendingChannelId, DerivationStrategyBase? derivationStrategyBase) + public async Task CreateOpenChannelRequest(ChannelOperationRequest channelOperationRequest, + PSBT? combinedPSBT, LightningNode? remoteNodeInfo, long fundingAmount, byte[] pendingChannelId, + DerivationStrategyBase? derivationStrategyBase) { if (combinedPSBT == null) throw new ArgumentNullException(nameof(combinedPSBT)); if (remoteNodeInfo == null) throw new ArgumentNullException(nameof(remoteNodeInfo)); @@ -683,12 +743,17 @@ public async Task CreateOpenChannelRequest(ChannelOperationR }; // Check features to see if we need or is allowed to add a close address - var upfrontShutdownScriptOpt = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptOpt); - var upfrontShutdownScriptReq = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptReq); - if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptOpt] is { IsKnown: true } || - upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptReq] is { IsKnown: true }) + var upfrontShutdownScriptOpt = + remoteNodeInfo.Features.ContainsKey((uint) FeatureBit.UpfrontShutdownScriptOpt); + var upfrontShutdownScriptReq = + remoteNodeInfo.Features.ContainsKey((uint) FeatureBit.UpfrontShutdownScriptReq); + if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint) FeatureBit.UpfrontShutdownScriptOpt] is + {IsKnown: true} || + upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint) FeatureBit.UpfrontShutdownScriptReq] is + {IsKnown: true}) { - var address = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); + var address = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, + _logger); openChannelRequest.CloseAddress = address.Address.ToString(); ; } @@ -700,14 +765,16 @@ public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationReque { //PSBT Combine var signedPsbts = channelOperationRequest.ChannelOperationRequestPsbts.Where(x => - channelOperationRequest.Wallet != null && !x.IsFinalisedPSBT && !x.IsInternalWalletPSBT && (channelOperationRequest.Wallet.IsHotWallet || !x.IsTemplatePSBT)); + channelOperationRequest.Wallet != null && !x.IsFinalisedPSBT && !x.IsInternalWalletPSBT && + (channelOperationRequest.Wallet.IsHotWallet || !x.IsTemplatePSBT)); var signedPsbts2 = signedPsbts.Select(x => x.PSBT); var combinedPSBT = LightningHelper.CombinePSBTs(signedPsbts2, _logger); if (combinedPSBT != null) return combinedPSBT; - var invalidPsbtNullToBeUsedForTheRequest = $"Invalid PSBT(null) to be used for the channel op request:{channelOperationRequest.Id}"; + var invalidPsbtNullToBeUsedForTheRequest = + $"Invalid PSBT(null) to be used for the channel op request:{channelOperationRequest.Id}"; _logger?.LogError(invalidPsbtNullToBeUsedForTheRequest); throw new ArgumentException(invalidPsbtNullToBeUsedForTheRequest, nameof(combinedPSBT)); @@ -721,13 +788,15 @@ public static async Task GetCloseAddress(ChannelOperationReq if (closeAddress != null) return closeAddress; - var closeAddressNull = $"Closing address was null for an operation on wallet:{channelOperationRequest.Wallet.Id}"; + var closeAddressNull = + $"Closing address was null for an operation on wallet:{channelOperationRequest.Wallet.Id}"; _logger?.LogError(closeAddressNull); throw new ArgumentException(closeAddressNull); } - public static DerivationStrategyBase GetDerivationStrategyBase(ChannelOperationRequest channelOperationRequest, ILogger? _logger = null) + public static DerivationStrategyBase GetDerivationStrategyBase(ChannelOperationRequest channelOperationRequest, + ILogger? _logger = null) { //Derivation strategy for the multisig address based on its wallet var derivationStrategyBase = channelOperationRequest.Wallet.GetDerivationStrategy(); @@ -741,20 +810,23 @@ public static DerivationStrategyBase GetDerivationStrategyBase(ChannelOperationR throw new ArgumentException(derivationNull); } - public static void CheckArgumentsAreValid(ChannelOperationRequest channelOperationRequest, OperationRequestType requestype, ILogger? _logger = null) + public static void CheckArgumentsAreValid(ChannelOperationRequest channelOperationRequest, + OperationRequestType requestype, ILogger? _logger = null) { if (channelOperationRequest == null) throw new ArgumentNullException(nameof(channelOperationRequest)); if (channelOperationRequest.RequestType == requestype) return; - string requestInvalid = $"Invalid request. Requested ${channelOperationRequest.RequestType.ToString()} on ${requestype.ToString()} method"; + string requestInvalid = + $"Invalid request. Requested ${channelOperationRequest.RequestType.ToString()} on ${requestype.ToString()} method"; _logger?.LogError(requestInvalid); throw new ArgumentOutOfRangeException(requestInvalid); } - public static (Node, Node) CheckNodesAreValid(ChannelOperationRequest channelOperationRequest, ILogger? _logger = null) + public static (Node, Node) CheckNodesAreValid(ChannelOperationRequest channelOperationRequest, + ILogger? _logger = null) { var source = channelOperationRequest.SourceNode; var destination = channelOperationRequest.DestNode; @@ -794,7 +866,8 @@ public static (Node, Node) CheckNodesAreValid(ChannelOperationRequest channelOpe /// public static async Task SignPSBTWithEmbeddedSigner( ChannelOperationRequest channelOperationRequest, INBXplorerService nbXplorerService, - DerivationStrategyBase derivationStrategyBase, Transaction channelfundingTx, Network network, PSBT changeFixedPSBT, ILogger? logger = null) + DerivationStrategyBase derivationStrategyBase, Transaction channelfundingTx, Network network, + PSBT changeFixedPSBT, ILogger? logger = null) { //We get the UTXO keyPath / derivation path from nbxplorer @@ -875,7 +948,8 @@ public static async Task SignPSBTWithEmbeddedSigner( /// /// /// - public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockable? client = null) + public void CancelPendingChannel(Node source, byte[] pendingChannelId, + IUnmockable? client = null) { try { @@ -897,7 +971,7 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab { ShimCancel = cancelRequest, }, - new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default)); + new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, default)); } } } @@ -983,16 +1057,23 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab if (!updateResult.Item1) { - _logger.LogError("Error while updating withdrawal request: {RequestId}", channelOperationRequest.Id); + _logger.LogError("Error while updating withdrawal request: {RequestId}", + channelOperationRequest.Id); } return (null, false); } } - var previouslyLockedUTXOs = await _coinSelectionService.GetLockedUTXOsForRequest(channelOperationRequest, BitcoinRequestType.ChannelOperation); - var availableUTXOs = previouslyLockedUTXOs.Count > 0 ? previouslyLockedUTXOs : await _coinSelectionService.GetAvailableUTXOsAsync(derivationStrategy); - var (multisigCoins, selectedUtxOs) = await _coinSelectionService.GetTxInputCoins(availableUTXOs, channelOperationRequest, derivationStrategy); + var previouslyLockedUTXOs = + await _coinSelectionService.GetLockedUTXOsForRequest(channelOperationRequest, + BitcoinRequestType.ChannelOperation); + var availableUTXOs = previouslyLockedUTXOs.Count > 0 + ? previouslyLockedUTXOs + : await _coinSelectionService.GetAvailableUTXOsAsync(derivationStrategy); + var (multisigCoins, selectedUtxOs) = + await _coinSelectionService.GetTxInputCoins(availableUTXOs, channelOperationRequest, + derivationStrategy); if (multisigCoins == null || !multisigCoins.Any()) { @@ -1012,12 +1093,16 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab var network = CurrentNetworkHelper.GetCurrentNetwork(); var txBuilder = network.CreateTransactionBuilder(); - var feeRateResult = channelOperationRequest.FeeRate ?? (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate.SatoshiPerByte; + var feeRateResult = channelOperationRequest.FeeRate ?? + (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate + .SatoshiPerByte; - var changeAddress = await _nbXplorerService.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false, default); + var changeAddress = await _nbXplorerService.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, + 0, false, default); if (changeAddress == null) { - _logger.LogError("Change address was not found for wallet: {WalletId}", channelOperationRequest.Wallet.Id); + _logger.LogError("Change address was not found for wallet: {WalletId}", + channelOperationRequest.Wallet.Id); return (null, false); } @@ -1056,7 +1141,8 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab input.SighashType = SigHash.None; } - var psbt = LightningHelper.AddDerivationData(channelOperationRequest.Wallet, result.Item1, selectedUtxOs, multisigCoins, _logger); + var psbt = LightningHelper.AddDerivationData(channelOperationRequest.Wallet, result.Item1, + selectedUtxOs, multisigCoins, _logger); result = (psbt, result.Item2); } catch (Exception e) @@ -1066,7 +1152,8 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab if (previouslyLockedUTXOs.Count == 0) { - await _coinSelectionService.LockUTXOs(selectedUtxOs, channelOperationRequest, BitcoinRequestType.ChannelOperation); + await _coinSelectionService.LockUTXOs(selectedUtxOs, channelOperationRequest, + BitcoinRequestType.ChannelOperation); } // The template PSBT is saved for later reuse @@ -1085,7 +1172,8 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab if (addPsbtResult.Item1 == false) { - _logger.LogError("Error while saving template PSBT to channel operation request: {RequestId}", channelOperationRequest.Id); + _logger.LogError("Error while saving template PSBT to channel operation request: {RequestId}", + channelOperationRequest.Id); } } @@ -1103,7 +1191,7 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, { if (channelOperationRequest.ChannelId != null) { - var channel = await _channelRepository.GetById((int)channelOperationRequest.ChannelId); + var channel = await _channelRepository.GetById((int) channelOperationRequest.ChannelId); var node = string.IsNullOrEmpty(channelOperationRequest.SourceNode.ChannelAdminMacaroon) ? channelOperationRequest.DestNode @@ -1122,7 +1210,7 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, OutputIndex = channel.FundingTxOutputIndex }, Force = forceClose, - }, new Metadata { { "macaroon", node.ChannelAdminMacaroon } }, null, default)); + }, new Metadata {{"macaroon", node.ChannelAdminMacaroon}}, null, default)); _logger.LogInformation("Channel close request: {RequestId} triggered", channelOperationRequest.Id); @@ -1145,7 +1233,8 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, channel.Id, closePendingTxid); - channelOperationRequest.Status = ChannelOperationRequestStatus.OnChainConfirmationPending; + channelOperationRequest.Status = + ChannelOperationRequestStatus.OnChainConfirmationPending; channelOperationRequest.TxId = closePendingTxid; var onChainPendingUpdate = @@ -1164,7 +1253,8 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, case CloseStatusUpdate.UpdateOneofCase.ChanClose: //TODO Review why chanclose.success it is false for confirmed closings of channels - var chanCloseClosingTxid = LightningHelper.DecodeTxId(response.ChanClose.ClosingTxid); + var chanCloseClosingTxid = + LightningHelper.DecodeTxId(response.ChanClose.ClosingTxid); _logger.LogInformation( "Channel close request in status: {RequestStatus} for channel operation request: {RequestId} for channel: {ChannelId} closing txId: {TxId}", nameof(ChannelOperationRequestStatus.OnChainConfirmed), @@ -1213,13 +1303,14 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, //We mark it as closed as it no longer exists if (channelOperationRequest.ChannelId != null) { - var channel = await _channelRepository.GetById((int)channelOperationRequest.ChannelId); + var channel = await _channelRepository.GetById((int) channelOperationRequest.ChannelId); if (channel != null) { channel.Status = Channel.ChannelStatus.Closed; _channelRepository.Update(channel); - _logger.LogInformation("Setting channel with id: {ChannelId} to closed as it no longer exists", + _logger.LogInformation( + "Setting channel with id: {ChannelId} to closed as it no longer exists", channel.Id); //It does not exists, probably was on-chain confirmed @@ -1305,7 +1396,7 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, { PubKey = pubkey, IncludeChannels = false - }, new Metadata { { "macaroon", node.ChannelAdminMacaroon } }, null, default)); + }, new Metadata {{"macaroon", node.ChannelAdminMacaroon}}, null, default)); result = nodeInfo?.Node; } @@ -1318,29 +1409,36 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, return result; } - public async Task<(long?, long?)> GetChannelBalance(Channel channel) + public async Task> GetChannelsBalance() { - IUnmockable client; - var destinationNode = await _nodeRepository.GetById(channel.DestinationNodeId); - var sourceNode = await _nodeRepository.GetById(channel.SourceNodeId); - var node = String.IsNullOrEmpty(sourceNode.ChannelAdminMacaroon) ? destinationNode : sourceNode; - - client = CreateLightningClient(node.Endpoint); - var result = client.Execute(x => x.ListChannels(new ListChannelsRequest(), - new Metadata + var nodes = await _nodeRepository.GetAllManagedByNodeGuard(); + + var result = new Dictionary(); + foreach (var node in nodes) + { + var client = CreateLightningClient(node.Endpoint); + var listChannelsResponse = client.Execute(x => x.ListChannels(new ListChannelsRequest(), + new Metadata + { + {"macaroon", node.ChannelAdminMacaroon} + }, null, default)); + + var channels = listChannelsResponse.Channels.ToList(); + + foreach (var channel in channels) { - { "macaroon", node.ChannelAdminMacaroon } - }, null, default)); + if (channel == null) continue; - var chan = result.Channels.FirstOrDefault(x => x.ChanId == channel.ChanId); - if (chan == null) - return (null, null); + var htlcsLocal = channel.PendingHtlcs.Where(x => x.Incoming == true).Sum(x => x.Amount); + var htlcsRemote = channel.PendingHtlcs.Where(x => x.Incoming == false).Sum(x => x.Amount); - var htlcsLocal = chan.PendingHtlcs.Where(x => x.Incoming == true).Sum(x => x.Amount); - var htlcsRemote = chan.PendingHtlcs.Where(x => x.Incoming == false).Sum(x => x.Amount); + result.TryAdd(channel.ChanId, + (node.Id, channel.LocalBalance + htlcsLocal, channel.RemoteBalance + htlcsRemote)); + } + } - var res = (chan.LocalBalance + htlcsLocal, chan.RemoteBalance + htlcsRemote); - return res; + + return result; } } } \ No newline at end of file