Skip to content

Commit

Permalink
Channel availability in channels table (#264)
Browse files Browse the repository at this point in the history
Channel availability in channels table
  • Loading branch information
RodriFS authored Aug 8, 2023
1 parent ce9c85f commit 77fc7b4
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 62 deletions.
8 changes: 8 additions & 0 deletions src/Helpers/ChannelStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NodeGuard.Helpers;

public class ChannelStatus
{
public long LocalBalance { get; set; }
public long RemoteBalance { get; set; }
public bool Active { get; set; }
}
131 changes: 76 additions & 55 deletions src/Pages/Channels.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
@using Channel = NodeGuard.Data.Models.Channel
@attribute [Authorize(Roles = "NodeManager")]
<PageTitle>Active Channels</PageTitle>
<h3 class="custom-primary">Channels</h3>
<h3 class="custom-primary">Channels
<Tooltip Class="ml-2" Text="Refresh channel information">
<Button Color="Color.Primary" Clicked="RefreshChannelInformation"><Icon Name="IconName.SyncAlt"></Icon></Button>
</Tooltip>
</h3>

<Row>
<Column ColumnSize="ColumnSize.Is12">
Expand Down Expand Up @@ -61,7 +65,6 @@
{
@(context.SourceNode.Name + " " + StringHelper.TruncateHeadAndTail(context.SourceNode.PubKey, 5))
}

</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Channel" Field="@nameof(Channel.DestinationNodeId)" Caption="Destination node" CustomFilter="OnDestinationNodeIdFilter" Filterable="true" Sortable="false" Displayable="@IsColumnVisible(ChannelsColumnName.DESTINATION_NODE)">
Expand All @@ -80,7 +83,6 @@
{
@(context.DestinationNode.Name + " " + StringHelper.TruncateHeadAndTail(context.DestinationNode.PubKey, 5))
}

</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Channel" Field="@nameof(Channel.Status)" Caption="@nameof(Channel.Status)" CustomFilter="OnStatusFilter" Filterable="true" Sortable="false" Displayable="@IsColumnVisible(ChannelsColumnName.STATUS)">
Expand Down Expand Up @@ -135,7 +137,6 @@
</DataGridNumericColumn>
<DataGridColumn TItem="Channel" Field="@nameof(Channel.IsPrivate)" Caption="Private" Filterable="false" Sortable="true" Displayable="@IsColumnVisible(ChannelsColumnName.PRIVATE)"/>
<DataGridColumn TItem="Channel" Field="@nameof(Channel.BtcCloseAddress)" Caption="Close address" Filterable="false" Sortable="false" Displayable="@IsColumnVisible(ChannelsColumnName.CLOSE_ADDRESS)">

<DisplayTemplate>
@if (!string.IsNullOrEmpty(context.BtcCloseAddress))
{
Expand All @@ -145,12 +146,26 @@
</Button>
}
</DisplayTemplate>

</DataGridColumn>
<DataGridColumn TItem="Channel" Field="@nameof(Channel.ChanId)" Caption="Availability" Sortable Filterable CustomFilter="OnAvailabilityFilter" Displayable="@IsColumnVisible(ChannelsColumnName.AVAILABILITY)">
<FilterTemplate>
<Select TValue="int" SelectedValue="@(_availabilityFilter)" SelectedValueChanged="@(value => { _availabilityFilter = value; context.TriggerFilterChange(_availabilityFilter); })">
<SelectItem Value="0">All</SelectItem>
<SelectItem Value="1">Active</SelectItem>
<SelectItem Value="-1">Inactive</SelectItem>
</Select>
</FilterTemplate>
<DisplayTemplate>
@{
var isActive = GetAvailability(context.ChanId);
}
<p>@(isActive ? "Active" : "Inactive")</p>
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Channel" Caption="Channel Balance" Filterable="false" Displayable="@IsColumnVisible(ChannelsColumnName.CHANNEL_BALANCE)">
<DisplayTemplate>
@{
var balance = Task.Run(() => GetPercentageBalance(context)).Result;
var balance = GetPercentageBalance(context);

}
@if (balance >= 0)
Expand All @@ -166,7 +181,6 @@
{
<p>Not available</p>
}

</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Channel" Field="@nameof(Channel.CreationDatetime)" Caption="@nameof(Channel.CreationDatetime).Humanize(LetterCasing.Sentence)" Sortable="true" Displayable="@IsColumnVisible(ChannelsColumnName.CREATION_DATE)"/>
Expand All @@ -187,6 +201,7 @@
</DataGrid>
</Column>
</Row>

<Modal @bind-Visible="@_modalVisible">
<ModalContent Centered Scrollable Size="ModalSize.ExtraLarge">
<ModalHeader>
Expand Down Expand Up @@ -224,7 +239,6 @@
{
<p>N/A</p>
}

</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="ChannelOperationRequest" Field="@nameof(ChannelOperationRequest.Status)" Caption="Status" Sortable="false" Displayable="true"/>
Expand All @@ -249,14 +263,11 @@
<ModalBody>
<Field>
<FieldLabel>Enable automated liquidity management</FieldLabel>
<Check TValue="bool" Checked="@_selectedChannel.IsAutomatedLiquidityEnabled" CheckedChanged="OnEnableLiquidityMgnmtChanged"></Check>

<Check TValue="bool" Checked="@_selectedChannel.IsAutomatedLiquidityEnabled" CheckedChanged="OnEnableLiquidityMngmtChanged"></Check>
</Field>

@if (_selectedChannel.IsAutomatedLiquidityEnabled && _currentLiquidityRule != null)
{
<Validations @ref="_channelManagementValidationsRef">

<Field>
<Validation Validator="ValidateLocalBalance">
<FieldLabel>Minimum local balance</FieldLabel>
Expand All @@ -268,7 +279,6 @@
</NumericEdit>
</Validation>
</Field>

<Field>
<Validation Validator="ValidateRemoteBalance">
<FieldLabel>Minimum remote balance</FieldLabel>
Expand All @@ -291,8 +301,6 @@
</NumericEdit>
</Validation>
</Field>


<Field>
<Validation Validator="ValidationRule.IsSelected">
<FieldLabel>Wallet to use in Swaps operations</FieldLabel>
Expand All @@ -310,10 +318,8 @@
</SelectList>
</Validation>
</Field>

</Validations>
}

</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary" Clicked="@CloseChannelManagementModal">Cancel</Button>
Expand Down Expand Up @@ -346,6 +352,7 @@
private LiquidityRule? _currentLiquidityRule = new LiquidityRule();
private Validations? _channelManagementValidationsRef;
private string _statusFilter = "Open";
private int _availabilityFilter = 0;
private int _sourceNodeIdFilter;
private int _destinationNodeIdFilter;
private List<Node> _nodes = new List<Node>();
Expand All @@ -364,8 +371,7 @@
private Dictionary<string, bool> _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<ulong, (int,long, long)> _channelsBalance = new();
private DateTimeOffset _lastBalanceUpdate = DateTimeOffset.Now;
private Dictionary<ulong, ChannelStatus> _channelsBalance = new();

public abstract class ChannelsColumnName
{
Expand All @@ -377,6 +383,7 @@
public static readonly ColumnDefault CAPACITY = new("Capacity (BTC)");
public static readonly ColumnDefault PRIVATE = new("Private");
public static readonly ColumnDefault CLOSE_ADDRESS = new("Close Address");
public static readonly ColumnDefault AVAILABILITY = new("Availability");
public static readonly ColumnDefault CHANNEL_BALANCE = new("Channel Balance");
public static readonly ColumnDefault CREATION_DATE = new("Creation Date");
public static readonly ColumnDefault UPDATE_DATE = new("Update Date");
Expand Down Expand Up @@ -410,7 +417,7 @@
_nodes = await NodeRepository.GetAll();
_wallets = await WalletRepository.GetAll();
_channelsDataGridRef?.FilterData();
_channelsBalance = await LightningService.GetChannelsBalance();
_channelsBalance = await LightningService.GetChannelsStatus();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand All @@ -430,7 +437,15 @@

private async Task ShowConfirmedClose(Channel? channel, bool forceClose = false)
{
if (await MessageService.Confirm($"Are you sure you want to {(forceClose ? "force " : string.Empty)}close this channel?", "Confirmation"))
await RefreshChannelInformation();
var isChannelActive = GetAvailability(channel?.ChanId);
var preMessage = "";
if (forceClose && isChannelActive)
{
preMessage = "This channel is active!. ";
}

if (await MessageService.Confirm($"{preMessage}Are you sure you want to {(forceClose ? "force " : string.Empty)}close this channel?", "Confirmation"))
{
if (channel != null)
{
Expand Down Expand Up @@ -473,44 +488,18 @@
ToastService.ShowSuccess("Text copied");
}

private async Task<int> GetPercentageBalance(Channel channel)
private int 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
{
var result = -1.0;
if (_channelsBalance.TryGetValue(channel.ChanId, out var values))
{
// Extract the managed node ID from the tuple
var managedNodeId = values.Item1;

// Calculate the capacity as the sum of the second and third values in the tuple
var capacity = values.Item2 + values.Item3;

// Initialize the remote balance
long remoteBalance;

// Check if the source node ID of the channel is the same as the managed node ID
// or if the source node is not managed
// If either condition is true, set the remote balance to the third value in the tuple
if (channel.SourceNodeId == managedNodeId || !channel.SourceNode.IsManaged)
{
remoteBalance = values.Item3;
}
else
{
// If neither condition is true, set the remote balance to the second value in the tuple
remoteBalance = values.Item2;
}
var capacity = values.LocalBalance + values.RemoteBalance;

// Calculate the result as the percentage of the remote balance to the capacity
result = (remoteBalance / (double) capacity) * 100;
result = (values.RemoteBalance / (double) capacity) * 100;


result = Math.Round(result, 2);
Expand All @@ -526,6 +515,23 @@
}
}

private bool GetAvailability(ulong? channelId)
{
if (channelId == null) return false;
try
{
if (_channelsBalance.TryGetValue(channelId.Value, out var value))
{
return value.Active;
}
}
catch (Exception)
{
ToastService.ShowError($"Channel availability for channel id:{channelId} could not be retrieved");
}
return false;
}

private async Task OnSelectedWalletChanged(int arg)
{
if (_currentLiquidityRule != null) _currentLiquidityRule.WalletId = arg;
Expand Down Expand Up @@ -622,13 +628,13 @@
if (_channelLiquidityModal != null) await _channelLiquidityModal.Show();
}

private async Task OnEnableLiquidityMgnmtChanged(bool enabledLiquidityMgnmt)
private async Task OnEnableLiquidityMngmtChanged(bool enabledLiquidityMngmt)
{
if (_selectedChannel != null)
{
_selectedChannel.IsAutomatedLiquidityEnabled = enabledLiquidityMgnmt;
_selectedChannel.IsAutomatedLiquidityEnabled = enabledLiquidityMngmt;

if (!enabledLiquidityMgnmt)
if (!enabledLiquidityMngmt)
{
_currentLiquidityRule = null;
}
Expand Down Expand Up @@ -823,10 +829,10 @@
private bool CheckDisableCloseChannelButton(Channel channel)
{
var lastRequest = channel.ChannelOperationRequests.LastOrDefault();

// If the channel is not created by the node guard, we dont disable the button
if (lastRequest == null || channel.CreatedByNodeGuard == false) return false;

// If the channel is created by the node guard, we disable the button if the channel is closed or if the last request is a close request and is confirmed or is pending confirmation
return channel.Status == Channel.ChannelStatus.Closed || (lastRequest.RequestType == OperationRequestType.Close
&& lastRequest.Status == ChannelOperationRequestStatus.OnChainConfirmed
Expand All @@ -847,4 +853,19 @@
return _channelsColumnLayout.IsColumnVisible(column);
}

private async Task RefreshChannelInformation()
{
_channelsBalance = await LightningService.GetChannelsStatus();
StateHasChanged();
}

private bool OnAvailabilityFilter(object? itemvalue, object? searchvalue)
{
if (searchvalue == null || (int)searchvalue == 0) return true;
var isActive = GetAvailability((ulong?)itemvalue);
if ((int)searchvalue == 1 && isActive) return true;
if ((int)searchvalue == -1 && !isActive) return true;
return false;
}

}
31 changes: 24 additions & 7 deletions src/Services/LightningService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public interface ILightningService
/// Gets a dictionary of the local and remote balance of all the channels managed by NG
/// </summary>
/// <returns></returns>
public Task<Dictionary<ulong, (int, long, long)>> GetChannelsBalance();
public Task<Dictionary<ulong, ChannelStatus>> GetChannelsStatus();

/// <summary>
/// Cancels a pending channel from LND PSBT-based funding of channels
Expand Down Expand Up @@ -1289,11 +1289,11 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest,
return await _lightningClientService.GetNodeInfo(node, pubkey);
}

public async Task<Dictionary<ulong, (int, long, long)>> GetChannelsBalance()
public async Task<Dictionary<ulong, ChannelStatus>> GetChannelsStatus()
{
var nodes = await _nodeRepository.GetAllManagedByNodeGuard();

var result = new Dictionary<ulong, (int, long, long)>();
var result = new Dictionary<ulong, ChannelStatus>();
foreach (var node in nodes)
{
var listChannelsResponse = await _lightningClientService.ListChannels(node);
Expand All @@ -1302,14 +1302,31 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest,
foreach (var channel in channels)
{
if (channel == null) continue;
// If the source node is not the channel initiator, but the remote node is also managed by NodeGuard
// We skip and wait for the other node to report the channel
if (nodes.Any((n) => !channel.Initiator && n.PubKey == channel.RemotePubkey)) continue;

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);

//The nodeguard sided node is the one that is managed by nodeguard
var nodeguardManagedNodeId = node.Id;
result.TryAdd(channel.ChanId,
(nodeguardManagedNodeId, channel.LocalBalance + htlcsLocal, channel.RemoteBalance + htlcsRemote));
var localBalance = channel.LocalBalance + htlcsLocal;
var remoteBalance = channel.RemoteBalance + htlcsRemote;

// If the channel is not initiated by a NodeGuard node, we need to swap the balances.
// the balance is always shown from the NodeGuard's perspective
if (!channel.Initiator)
{
localBalance = channel.RemoteBalance + htlcsRemote;
remoteBalance = channel.LocalBalance + htlcsLocal;
}


result.TryAdd(channel.ChanId, new ChannelStatus()
{
LocalBalance = localBalance,
RemoteBalance = remoteBalance,
Active = channel.Active
});
}
}

Expand Down
Loading

0 comments on commit 77fc7b4

Please sign in to comment.