From f997808a46655b5274dc30eaacc33c9932443ef1 Mon Sep 17 00:00:00 2001 From: ardevd Date: Wed, 14 Feb 2024 22:54:30 +0100 Subject: [PATCH] feat: show pending channels on dashboard (#40) --- .github/workflows/sonarqube.yml | 4 +- internal/lnd/data.go | 16 +++++-- internal/lnd/pending_channel.go | 51 ++++++++++++++++++++ internal/tui/dashboard.go | 52 ++++++++++++++------ internal/tui/tui.go | 84 +++++++++++++++++++++++++++++---- 5 files changed, 177 insertions(+), 30 deletions(-) create mode 100644 internal/lnd/pending_channel.go diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index ab35d36..afa94fc 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -44,8 +44,8 @@ jobs: - name: Analyze with SonarQube # You can pin the exact commit or the version. - # uses: SonarSource/sonarqube-scan-action@v1.1.0 - uses: SonarSource/sonarqube-scan-action@7295e71c9583053f5bf40e9d4068a0c974603ec8 + # uses: SonarSource/sonarqube-scan-action@master + uses: SonarSource/sonarqube-scan-action@master env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on SonarQube, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) diff --git a/internal/lnd/data.go b/internal/lnd/data.go index ad29768..1733848 100644 --- a/internal/lnd/data.go +++ b/internal/lnd/data.go @@ -3,9 +3,10 @@ package lnd import "github.com/charmbracelet/bubbles/list" type NodeData struct { - NodeInfo Node - Channels []Channel - Payments []Payment + NodeInfo Node + Channels []Channel + PendingChannels []PendingChannel + Payments []Payment } func (n NodeData) GetChannelsAsListItems(onlyOffline bool) []list.Item { @@ -19,6 +20,15 @@ func (n NodeData) GetChannelsAsListItems(onlyOffline bool) []list.Item { return channelItems } +func (n NodeData) GetPendingChannelsAsListItems() []list.Item { + var pendingChannelItems []list.Item + for _, pendingChannel := range n.PendingChannels { + pendingChannelItems = append(pendingChannelItems, pendingChannel) + } + + return pendingChannelItems +} + func (n NodeData) GetPaymentsAsListItems() []list.Item { var paymentItems []list.Item for _, payment := range n.Payments { diff --git a/internal/lnd/pending_channel.go b/internal/lnd/pending_channel.go new file mode 100644 index 0000000..7fa5b80 --- /dev/null +++ b/internal/lnd/pending_channel.go @@ -0,0 +1,51 @@ +package lnd + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" +) + +// ChannelType represents the type of Lightning network channel. +type PendingChannelType int + +const ( + // PendingOpen indicates a pending channel opening. + PendingOpen PendingChannelType = iota + // CooperativeClosure indicates a cooperative channel closure. + CooperativeClosure + // ForceClosure indicates a forceful channel closure. + ForceClosure +) + +type PendingChannel struct { + Capacity btcutil.Amount + LocalBalance btcutil.Amount + RecoveredBalance btcutil.Amount + LimboBalance btcutil.Amount + BlocksUntilMaturity int32 + Type PendingChannelType + Alias string +} + +func (c PendingChannel) FilterValue() string { + return c.Alias +} + +func (c PendingChannel) Title() string { + return c.Alias +} + +func (c PendingChannel) Description() string { + switch c.Type { + case PendingOpen: + return "Opening" + case CooperativeClosure: + return "Closing" + case ForceClosure: + return fmt.Sprintf("Force closing (%d sats in limbo)", + int(c.LimboBalance.ToUnit(btcutil.AmountSatoshi))) + } + + return "" +} diff --git a/internal/tui/dashboard.go b/internal/tui/dashboard.go index 165e780..6f64b95 100644 --- a/internal/tui/dashboard.go +++ b/internal/tui/dashboard.go @@ -17,6 +17,7 @@ type dashboardComponent int const ( channels dashboardComponent = iota payments + pendingChannels nodeinfo messageTools channelTools @@ -33,10 +34,13 @@ func InitDashboard(service *lndclient.GrpcLndServices, nodeData lnd.NodeData) *D func (m *DashboardModel) initData(width, height int) { - defaultList := list.New([]list.Item{}, list.NewDefaultDelegate(), width, height/2) + adjustedHeight := height + height/3 + adjustedCompressedHeight := height + height/2 + defaultList := list.New([]list.Item{}, list.NewDefaultDelegate(), width, adjustedHeight/2) + compressedList := list.New([]list.Item{}, list.NewDefaultDelegate(), width, adjustedCompressedHeight/5) defaultList.SetShowHelp(true) - m.lists = []list.Model{defaultList, defaultList} + m.lists = []list.Model{defaultList, compressedList, compressedList} m.forms = []*huh.Form{m.generatePaymentToolsForm(), m.generateChannelToolsForm(), m.generateMessageToolsForm()} m.lists[channels].Title = "Channels" @@ -50,6 +54,9 @@ func (m *DashboardModel) initData(width, height int) { m.lists[payments].Title = "Latest Payments" m.lists[payments].SetItems(m.nodeData.GetPaymentsAsListItems()) + m.lists[pendingChannels].Title = "Pending Channels" + m.lists[pendingChannels].SetItems(m.nodeData.GetPendingChannelsAsListItems()) + m.base = *NewBaseModel(m) } @@ -122,12 +129,30 @@ func (m DashboardModel) Init() tea.Cmd { return nil } +func (m DashboardModel) getCompressedListViews() string { + s := m.styles + switch m.focused { + case payments: + return lipgloss.JoinVertical(lipgloss.Center, + s.FocusedStyle.Render(m.lists[payments].View()), + s.BorderedStyle.Render(m.lists[pendingChannels].View())) + case pendingChannels: + return lipgloss.JoinVertical(lipgloss.Center, + s.BorderedStyle.Render(m.lists[payments].View()), + s.FocusedStyle.Render(m.lists[pendingChannels].View())) + default: + return lipgloss.JoinVertical(lipgloss.Center, + s.BorderedStyle.Render(m.lists[payments].View()), + s.BorderedStyle.Render(m.lists[pendingChannels].View())) + } + +} + func (m DashboardModel) View() string { s := m.styles if m.loaded { channelsView := m.lists[channels].View() - paymentsView := m.lists[payments].View() var listsView string switch m.focused { @@ -136,21 +161,14 @@ func (m DashboardModel) View() string { listsView = lipgloss.JoinHorizontal( lipgloss.Center, s.FocusedStyle.Render(channelsView), - s.BorderedStyle.Render(paymentsView), - ) - - case payments: - listsView = lipgloss.JoinHorizontal( - lipgloss.Center, - s.BorderedStyle.Render(channelsView), - s.FocusedStyle.Render(paymentsView), + m.getCompressedListViews(), ) default: listsView = lipgloss.JoinHorizontal( lipgloss.Center, s.BorderedStyle.Render(channelsView), - s.BorderedStyle.Render(paymentsView), + m.getCompressedListViews(), ) } @@ -264,10 +282,10 @@ func (m *DashboardModel) handleFormClick(component dashboardComponent) (tea.Mode m.forms[0] = m.generatePaymentToolsForm() case messageTools: if m.forms[2].GetString("messages") == OPTION_MESSAGE_SIGN { - + i = newSignMessageModel(m.lndService, &m.base) } else { - + i = newVerifyMessageModel(m.lndService, &m.base) } m.forms[2] = m.generateMessageToolsForm() @@ -285,8 +303,10 @@ func (m *DashboardModel) Prev() { m.focused = messageTools case payments: m.focused = channels - case paymentTools: + case pendingChannels: m.focused = payments + case paymentTools: + m.focused = pendingChannels case channelTools: m.focused = paymentTools case messageTools: @@ -299,6 +319,8 @@ func (m *DashboardModel) Next() { case channels: m.focused = payments case payments: + m.focused = pendingChannels + case pendingChannels: m.focused = paymentTools case paymentTools: m.focused = channelTools diff --git a/internal/tui/tui.go b/internal/tui/tui.go index bdc2199..6819250 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -7,6 +7,7 @@ import ( "github.com/ardevd/flash/internal/lnd" tea "github.com/charmbracelet/bubbletea" "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/routing/route" ) var windowSizeMsg tea.WindowSizeMsg @@ -16,13 +17,16 @@ var Models []tea.Model // Message types type DataLoaded lnd.NodeData + // Payments type paymentSettled struct{} type paymentExpired struct{} type paymentCreated struct{} type paymentError struct{} + // Channel type updateChannelPolicy struct{} + func updateChannelPolicyMsg() tea.Msg { return updateChannelPolicy{} } @@ -45,7 +49,10 @@ func GetData(service *lndclient.GrpcLndServices, ctx context.Context) lnd.NodeDa nodeData.Payments = paymentsSlice // Load Channels - nodeData.Channels = GetChannelListItems(service, ctx) + nodeData.Channels = getChannelListItems(service, ctx) + + // Load Pending channels + nodeData.PendingChannels = getPendingChannels(service, ctx) // Load node data nodeData.NodeInfo = lnd.GetDataFromAPI(service, ctx) @@ -53,7 +60,70 @@ func GetData(service *lndclient.GrpcLndServices, ctx context.Context) lnd.NodeDa return nodeData } -func GetChannelListItems(service *lndclient.GrpcLndServices, ctx context.Context) []lnd.Channel { +// Get list of pending channels +func getPendingChannels(service *lndclient.GrpcLndServices, ctx context.Context) []lnd.PendingChannel { + var pendingChannels []lnd.PendingChannel + channels, err := service.Client.PendingChannels(ctx) + if err != nil { + log.Fatal(err) + } + + // Force close channels + for _, fc := range channels.PendingForceClose { + remotePeerAlias := getNodeAliasFromPubKey(service, ctx, fc.PubKeyBytes) + + pendingChannel := lnd.PendingChannel{ + Capacity: fc.Capacity, + LocalBalance: fc.LocalBalance, + RecoveredBalance: fc.RecoveredBalance, + LimboBalance: fc.LimboBalance, + BlocksUntilMaturity: fc.BlocksUntilMaturity, + Type: lnd.ForceClosure, + Alias: remotePeerAlias, + } + pendingChannels = append(pendingChannels, pendingChannel) + } + + // Cooperative closing channels + for _, fc := range channels.WaitingClose { + remotePeerAlias := getNodeAliasFromPubKey(service, ctx, fc.PubKeyBytes) + + pendingChannel := lnd.PendingChannel{ + Capacity: fc.Capacity, + LocalBalance: fc.LocalBalance, + Type: lnd.CooperativeClosure, + Alias: remotePeerAlias, + } + pendingChannels = append(pendingChannels, pendingChannel) + } + + // Pending channel opens + for _, fc := range channels.PendingOpen { + remotePeerAlias := getNodeAliasFromPubKey(service, ctx, fc.PubKeyBytes) + + pendingChannel := lnd.PendingChannel{ + Capacity: fc.Capacity, + LocalBalance: fc.LocalBalance, + Type: lnd.CooperativeClosure, + Alias: remotePeerAlias, + } + pendingChannels = append(pendingChannels, pendingChannel) + } + + return pendingChannels +} + +func getNodeAliasFromPubKey(service *lndclient.GrpcLndServices, ctx context.Context, pubKey route.Vertex) string { + node, err := service.Client.GetNodeInfo(ctx, pubKey, true) + if err != nil { + // TODO: add logging + return "" + } + + return node.Alias +} + +func getChannelListItems(service *lndclient.GrpcLndServices, ctx context.Context) []lnd.Channel { var channels []lnd.Channel infos, err := service.Client.ListChannels(ctx, false, false) if err != nil { @@ -61,13 +131,7 @@ func GetChannelListItems(service *lndclient.GrpcLndServices, ctx context.Context } for _, chanInfo := range infos { - remotePeerAlias := "" - channelNode, err := service.Client.GetNodeInfo(ctx, chanInfo.PubKeyBytes, true) - if err != nil { - // TODO: add logging - } else { - remotePeerAlias = channelNode.Alias - } + remotePeerAlias := getNodeAliasFromPubKey(service, ctx, chanInfo.PubKeyBytes) channels = append(channels, lnd.Channel{Info: chanInfo, Alias: remotePeerAlias}) } @@ -86,6 +150,6 @@ const ( OPTION_PAYMENT_SEND = "send" OPTION_MESSAGE_SIGN = "sign" OPTION_MESSAGE_VERIFY = "verify" - OPTION_CHANNEL_OPEN = "open" + OPTION_CHANNEL_OPEN = "open" OPTION_CONNECT_TO_PEER = "connect" )