From 266bad658c5b5dc96173e55eafebd31026b20b33 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Sun, 9 May 2021 18:40:30 +0200 Subject: [PATCH 1/3] cli: add usage --- cli/cli.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/cli.go b/cli/cli.go index d84c1ba..e5b0211 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -27,6 +27,7 @@ func New() *cli.App { return &cli.App{ Name: "lntop", Version: version, + Usage: "LN channels viewer", EnableShellCompletion: true, Action: run, Flags: []cli.Flag{ From f0dedd5d2a482725a6a0929ea34b54e6fd88a878 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Sun, 9 May 2021 23:37:01 +0200 Subject: [PATCH 2/3] add routing view --- README.md | 31 +++ config/config.go | 1 + config/default.go | 18 ++ events/events.go | 6 + network/backend/backend.go | 2 + network/backend/lnd/lnd.go | 54 ++++ network/backend/lnd/proto.go | 66 +++++ network/backend/mock/mock.go | 4 + network/models/routingevent.go | 45 ++++ pubsub/pubsub.go | 30 +++ ui/controller.go | 14 + ui/models/models.go | 33 +++ ui/views/menu.go | 3 + ui/views/routing.go | 464 +++++++++++++++++++++++++++++++++ ui/views/views.go | 4 + 15 files changed, 775 insertions(+) create mode 100644 network/models/routingevent.go create mode 100644 ui/views/routing.go diff --git a/README.md b/README.md index 59cce47..524c838 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,39 @@ columns = [ "FEE", # fee of the transaction "ADDRESSES", # number of transaction output addresses ] + +[views.routing] +columns = [ + "DIR", # event type: send, receive, forward + "STATUS", # one of: active, settled, failed, linkfail + "IN_CHANNEL", # channel id of the incomming channel + "IN_ALIAS", # incoming channel node alias + # "IN_HTLC", # htlc id on incoming channel + # "IN_TIMELOCK", # incoming timelock height + "OUT_CHANNEL", # channel id of the outgoing channel + "OUT_ALIAS", # outgoing channel node alias + # "OUT_HTLC", # htlc id on outgoing channel + # "OUT_TIMELOCK", # outgoing timelock height + "AMOUNT", # routed amount + "FEE", # routing fee + "LAST UPDATE", # last update + "DETAIL", # error description +] ``` +## Routing view + +Routing view displays screenful of latest routing events. This information +is not persisted in LND so the view always starts empty and is lost once +you exit `lntop`. + +The events are in one of four states: + +* `active` - HTLC pending +* `settled` - preimage revealed, HTLC removed +* `failed` - payment failed at a downstream node +* `linkfail` - payment failed at this node + ## Docker If you prefer to run `lntop` from a docker container, `cd docker` and follow [`README`](docker/README.md) there. diff --git a/config/config.go b/config/config.go index 9a0498e..a918d05 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,7 @@ type Network struct { type Views struct { Channels *View `toml:"channels"` Transactions *View `toml:"transactions"` + Routing *View `toml:"routing"` } type View struct { diff --git a/config/default.go b/config/default.go index 896734d..b012504 100644 --- a/config/default.go +++ b/config/default.go @@ -56,6 +56,24 @@ columns = [ "FEE", # fee of the transaction "ADDRESSES", # number of transaction output addresses ] + +[views.routing] +columns = [ + "DIR", # event type: send, receive, forward + "STATUS", # one of: active, settled, failed, linkfail + "IN_CHANNEL", # channel id of the incomming channel + "IN_ALIAS", # incoming channel node alias + # "IN_HTLC", # htlc id on incoming channel + # "IN_TIMELOCK", # incoming timelock height + "OUT_CHANNEL", # channel id of the outgoing channel + "OUT_ALIAS", # outgoing channel node alias + # "OUT_HTLC", # htlc id on outgoing channel + # "OUT_TIMELOCK", # outgoing timelock height + "AMOUNT", # routed amount + "FEE", # routing fee + "LAST UPDATE", # last update + "DETAIL", # error description +] `, cfg.Logger.Type, cfg.Logger.Dest, diff --git a/events/events.go b/events/events.go index 3e675a2..e20d295 100644 --- a/events/events.go +++ b/events/events.go @@ -11,13 +11,19 @@ const ( PeerUpdated = "peer.updated" TransactionCreated = "transaction.created" WalletBalanceUpdated = "wallet.balance.updated" + RoutingEventUpdated = "routing.event.updated" ) type Event struct { Type string ID string + Data interface{} } func New(kind string) *Event { return &Event{Type: kind} } + +func NewWithData(kind string, data interface{}) *Event { + return &Event{Type: kind, Data: data} +} diff --git a/network/backend/backend.go b/network/backend/backend.go index 1318ec7..b82fc79 100644 --- a/network/backend/backend.go +++ b/network/backend/backend.go @@ -39,4 +39,6 @@ type Backend interface { GetTransactions(context.Context) ([]*models.Transaction, error) SubscribeTransactions(context.Context, chan *models.Transaction) error + + SubscribeRoutingEvents(context.Context, chan *models.RoutingEvent) error } diff --git a/network/backend/lnd/lnd.go b/network/backend/lnd/lnd.go index f872a89..a33b457 100644 --- a/network/backend/lnd/lnd.go +++ b/network/backend/lnd/lnd.go @@ -6,6 +6,7 @@ import ( "time" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -31,6 +32,15 @@ func (c *Client) Close() error { return c.conn.Close() } +type RouterClient struct { + routerrpc.RouterClient + conn *pool.Conn +} + +func (c *RouterClient) Close() error { + return c.conn.Close() +} + type Backend struct { cfg *config.Network logger logging.Logger @@ -150,6 +160,38 @@ func (l Backend) SubscribeChannels(ctx context.Context, events chan *models.Chan return nil } +func (l Backend) SubscribeRoutingEvents(ctx context.Context, channelEvents chan *models.RoutingEvent) error { + clt, err := l.RouterClient(ctx) + if err != nil { + return err + } + defer clt.Close() + + cltRoutingEvents, err := clt.SubscribeHtlcEvents(ctx, &routerrpc.SubscribeHtlcEventsRequest{}) + if err != nil { + return err + } + + for { + select { + case <-ctx.Done(): + break + default: + event, err := cltRoutingEvents.Recv() + if err != nil { + st, ok := status.FromError(err) + if ok && st.Code() == codes.Canceled { + l.logger.Debug("stopping subscribe routing events: context canceled") + return nil + } + return err + } + + channelEvents <- protoToRoutingEvent(event) + } + } +} + func (l Backend) Client(ctx context.Context) (*Client, error) { conn, err := l.pool.Get(ctx) if err != nil { @@ -162,6 +204,18 @@ func (l Backend) Client(ctx context.Context) (*Client, error) { }, nil } +func (l Backend) RouterClient(ctx context.Context) (*RouterClient, error) { + conn, err := l.pool.Get(ctx) + if err != nil { + return nil, err + } + + return &RouterClient{ + RouterClient: routerrpc.NewRouterClient(conn.ClientConn), + conn: conn, + }, nil +} + func (l Backend) NewClientConn() (*grpc.ClientConn, error) { return newClientConn(l.cfg) } diff --git a/network/backend/lnd/proto.go b/network/backend/lnd/proto.go index 36cf2a7..c003be3 100644 --- a/network/backend/lnd/proto.go +++ b/network/backend/lnd/proto.go @@ -1,9 +1,11 @@ package lnd import ( + "fmt" "time" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/edouardparis/lntop/network/models" ) @@ -314,3 +316,67 @@ func protoToTransaction(resp *lnrpc.Transaction) *models.Transaction { DestAddresses: resp.DestAddresses, } } + +func protoToRoutingEvent(resp *routerrpc.HtlcEvent) *models.RoutingEvent { + var status, direction int + var incomingMsat, outgoingMsat uint64 + var incomingTimelock, outgoingTimelock uint32 + var amountMsat, feeMsat uint64 + var failureCode int32 + var detail string + + if fe := resp.GetForwardEvent(); fe != nil { + status = models.RoutingStatusActive + incomingMsat = fe.Info.IncomingAmtMsat + outgoingMsat = fe.Info.OutgoingAmtMsat + incomingTimelock = fe.Info.IncomingTimelock + outgoingTimelock = fe.Info.OutgoingTimelock + } else if ffe := resp.GetForwardFailEvent(); ffe != nil { + status = models.RoutingStatusFailed + } else if se := resp.GetSettleEvent(); se != nil { + status = models.RoutingStatusSettled + } else if lfe := resp.GetLinkFailEvent(); lfe != nil { + incomingMsat = lfe.Info.IncomingAmtMsat + outgoingMsat = lfe.Info.OutgoingAmtMsat + incomingTimelock = lfe.Info.IncomingTimelock + outgoingTimelock = lfe.Info.OutgoingTimelock + status = models.RoutingStatusLinkFailed + detail = lfe.WireFailure.String() + if s := lfe.FailureDetail.String(); s != "" { + detail = fmt.Sprintf("%s %s", detail, s) + } + if lfe.FailureString != "" { + detail = fmt.Sprintf("%s %s", detail, lfe.FailureString) + } + failureCode = int32(lfe.WireFailure) + } + + switch resp.EventType { + case routerrpc.HtlcEvent_SEND: + direction = models.RoutingSend + amountMsat = outgoingMsat + case routerrpc.HtlcEvent_RECEIVE: + direction = models.RoutingReceive + amountMsat = incomingMsat + case routerrpc.HtlcEvent_FORWARD: + direction = models.RoutingForward + amountMsat = outgoingMsat + feeMsat = incomingMsat - outgoingMsat + } + + return &models.RoutingEvent{ + IncomingChannelId: resp.IncomingChannelId, + OutgoingChannelId: resp.OutgoingChannelId, + IncomingHtlcId: resp.IncomingHtlcId, + OutgoingHtlcId: resp.OutgoingHtlcId, + LastUpdate: time.Unix(0, int64(resp.TimestampNs)), + Direction: direction, + Status: status, + IncomingTimelock: incomingTimelock, + OutgoingTimelock: outgoingTimelock, + AmountMsat: amountMsat, + FeeMsat: feeMsat, + FailureCode: failureCode, + FailureDetail: detail, + } +} diff --git a/network/backend/mock/mock.go b/network/backend/mock/mock.go index 10e507b..8124b4f 100644 --- a/network/backend/mock/mock.go +++ b/network/backend/mock/mock.go @@ -50,6 +50,10 @@ func (b *Backend) SubscribeTransactions(ctx context.Context, channel chan *model return nil } +func (b *Backend) SubscribeRoutingEvents(ctx context.Context, channel chan *models.RoutingEvent) error { + return nil +} + func (b *Backend) GetNode(ctx context.Context, pubkey string) (*models.Node, error) { return &models.Node{}, nil } diff --git a/network/models/routingevent.go b/network/models/routingevent.go new file mode 100644 index 0000000..c5fff3a --- /dev/null +++ b/network/models/routingevent.go @@ -0,0 +1,45 @@ +package models + +import ( + "time" +) + +const ( + RoutingSend = iota + 1 + RoutingReceive + RoutingForward +) + +const ( + RoutingStatusActive = iota + 1 + RoutingStatusFailed + RoutingStatusSettled + RoutingStatusLinkFailed +) + +type RoutingEvent struct { + IncomingChannelId uint64 + OutgoingChannelId uint64 + IncomingHtlcId uint64 + OutgoingHtlcId uint64 + LastUpdate time.Time + Direction int + Status int + IncomingTimelock uint32 + OutgoingTimelock uint32 + AmountMsat uint64 + FeeMsat uint64 + FailureCode int32 + FailureDetail string +} + +func (u *RoutingEvent) Equals(other *RoutingEvent) bool { + return u.IncomingChannelId == other.IncomingChannelId && u.IncomingHtlcId == other.IncomingHtlcId && u.OutgoingChannelId == other.OutgoingChannelId && u.OutgoingHtlcId == other.OutgoingHtlcId +} + +func (u *RoutingEvent) Update(newer *RoutingEvent) { + u.LastUpdate = newer.LastUpdate + u.Status = newer.Status + u.FailureCode = newer.FailureCode + u.FailureDetail = newer.FailureDetail +} diff --git a/pubsub/pubsub.go b/pubsub/pubsub.go index dc840ff..0d32459 100644 --- a/pubsub/pubsub.go +++ b/pubsub/pubsub.go @@ -88,6 +88,35 @@ func (p *PubSub) transactions(ctx context.Context, sub chan *events.Event) { }() } +func (p *PubSub) routingUpdates(ctx context.Context, sub chan *events.Event) { + p.wg.Add(3) + routingUpdates := make(chan *models.RoutingEvent) + ctx, cancel := context.WithCancel(ctx) + + go func() { + for hu := range routingUpdates { + p.logger.Debug("receive htlcUpdate") + sub <- events.NewWithData(events.RoutingEventUpdated, hu) + } + p.wg.Done() + }() + + go func() { + err := p.network.SubscribeRoutingEvents(ctx, routingUpdates) + if err != nil { + p.logger.Error("SubscribeRoutingEvents returned an error", logging.Error(err)) + } + p.wg.Done() + }() + + go func() { + <-p.stop + cancel() + close(routingUpdates) + p.wg.Done() + }() +} + func (p *PubSub) Stop() { p.stop <- true close(p.stop) @@ -99,6 +128,7 @@ func (p *PubSub) Run(ctx context.Context, sub chan *events.Event) { p.invoices(ctx, sub) p.transactions(ctx, sub) + p.routingUpdates(ctx, sub) p.ticker(ctx, sub, withTickerInfo(), withTickerChannelsBalance(), diff --git a/ui/controller.go b/ui/controller.go index c676b6c..9106136 100644 --- a/ui/controller.go +++ b/ui/controller.go @@ -144,6 +144,8 @@ func (c *controller) Listen(ctx context.Context, g *gocui.Gui, sub chan *events. ) case events.PeerUpdated: refresh(c.models.RefreshInfo) + case events.RoutingEventUpdated: + refresh(c.models.RefreshRouting(event.Data)) } } } @@ -270,7 +272,19 @@ func (c *controller) OnEnter(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } + case views.ROUTING: + err := c.views.Main.Delete(g) + if err != nil { + return err + } + + c.views.Main = c.views.Routing + err = c.views.Routing.Set(g, 11, 6, maxX-1, maxY) + if err != nil { + return err + } } + case views.TRANSACTIONS: index := c.views.Transactions.Index() c.models.Transactions.SetCurrent(index) diff --git a/ui/models/models.go b/ui/models/models.go index ca7af8a..793e65e 100644 --- a/ui/models/models.go +++ b/ui/models/models.go @@ -18,6 +18,7 @@ type Models struct { WalletBalance *WalletBalance ChannelsBalance *ChannelsBalance Transactions *Transactions + RoutingLog *RoutingLog } func New(app *app.App) *Models { @@ -29,6 +30,7 @@ func New(app *app.App) *Models { WalletBalance: &WalletBalance{}, ChannelsBalance: &ChannelsBalance{}, Transactions: &Transactions{}, + RoutingLog: &RoutingLog{}, } } @@ -103,3 +105,34 @@ func (m *Models) RefreshChannelsBalance(ctx context.Context) error { *m.ChannelsBalance = ChannelsBalance{balance} return nil } + +type RoutingLog struct { + Log []*models.RoutingEvent +} + +const MaxRoutingEvents = 512 // 8K monitor @ 8px per line = 540 + +func (m *Models) RefreshRouting(update interface{}) func(context.Context) error { + return (func(ctx context.Context) error { + hu, ok := update.(*models.RoutingEvent) + if ok { + found := false + for _, hlu := range m.RoutingLog.Log { + if hlu.Equals(hu) { + hlu.Update(hu) + found = true + break + } + } + if !found { + if len(m.RoutingLog.Log) == MaxRoutingEvents { + m.RoutingLog.Log = m.RoutingLog.Log[1:] + } + m.RoutingLog.Log = append(m.RoutingLog.Log, hu) + } + } else { + m.logger.Error("refreshRouting: invalid event data") + } + return nil + }) +} diff --git a/ui/views/menu.go b/ui/views/menu.go index 6f67c86..49d2116 100644 --- a/ui/views/menu.go +++ b/ui/views/menu.go @@ -16,6 +16,7 @@ const ( var menu = []string{ "CHANNEL", "TRANSAC", + "ROUTING", } type Menu struct { @@ -76,6 +77,8 @@ func (h Menu) Current() string { return CHANNELS case "TRANSAC": return TRANSACTIONS + case "ROUTING": + return ROUTING } } return "" diff --git a/ui/views/routing.go b/ui/views/routing.go new file mode 100644 index 0000000..0dc8a8d --- /dev/null +++ b/ui/views/routing.go @@ -0,0 +1,464 @@ +package views + +import ( + "bytes" + "fmt" + + "github.com/jroimartin/gocui" + "golang.org/x/text/language" + "golang.org/x/text/message" + + "github.com/edouardparis/lntop/config" + netmodels "github.com/edouardparis/lntop/network/models" + "github.com/edouardparis/lntop/ui/color" + "github.com/edouardparis/lntop/ui/models" +) + +const ( + ROUTING = "routing" + ROUTING_COLUMNS = "routing_columns" + ROUTING_FOOTER = "routing_footer" +) + +var DefaultRoutingColumns = []string{ + "DIR", + "STATUS", + "IN_CHANNEL", + "IN_ALIAS", + "OUT_CHANNEL", + "OUT_ALIAS", + "AMOUNT", + "FEE", + "LAST UPDATE", + "DETAIL", +} + +type Routing struct { + cfg *config.View + + columns []routingColumn + + columnsView *gocui.View + view *gocui.View + routingEvents *models.RoutingLog + + ox, oy int + cx, cy int +} + +type routingColumn struct { + name string + width int + display func(*netmodels.RoutingEvent, ...color.Option) string +} + +func (c Routing) Name() string { + return ROUTING +} + +func (c *Routing) Wrap(v *gocui.View) View { + c.view = v + return c +} + +func (c Routing) currentColumnIndex() int { + x := c.ox + c.cx + index := 0 + sum := 0 + for i := range c.columns { + sum += c.columns[i].width + 1 + if x < sum { + return index + } + index++ + } + return index +} + +func (c Routing) Origin() (int, int) { + return c.ox, c.oy +} + +func (c Routing) Cursor() (int, int) { + return c.cx, c.cy +} + +func (c *Routing) SetCursor(cx, cy int) error { + err := c.columnsView.SetCursor(cx, 0) + if err != nil { + return err + } + + err = c.view.SetCursor(cx, cy) + if err != nil { + return err + } + + c.cx, c.cy = cx, cy + return nil +} + +func (c *Routing) SetOrigin(ox, oy int) error { + err := c.columnsView.SetOrigin(ox, 0) + if err != nil { + return err + } + err = c.view.SetOrigin(ox, oy) + if err != nil { + return err + } + + c.ox, c.oy = ox, oy + return nil +} + +func (c *Routing) Speed() (int, int, int, int) { + _, height := c.view.Size() + current := c.currentColumnIndex() + up := 0 + down := 0 + if c.Index() > 0 { + up = 1 + } + if c.Index() < len(c.routingEvents.Log)-1 && c.Index() < height { + down = 1 + } + if current > len(c.columns)-1 { + return 0, c.columns[current-1].width + 1, down, up + } + if current == 0 { + return c.columns[0].width + 1, 0, down, up + } + return c.columns[current].width + 1, + c.columns[current-1].width + 1, + down, up +} + +func (c Routing) Index() int { + _, oy := c.Origin() + _, cy := c.Cursor() + return cy + oy +} + +func (c Routing) Delete(g *gocui.Gui) error { + err := g.DeleteView(ROUTING_COLUMNS) + if err != nil { + return err + } + + err = g.DeleteView(ROUTING) + if err != nil { + return err + } + + return g.DeleteView(ROUTING_FOOTER) +} + +func (c *Routing) Set(g *gocui.Gui, x0, y0, x1, y1 int) error { + var err error + setCursor := false + c.columnsView, err = g.SetView(ROUTING_COLUMNS, x0-1, y0, x1+2, y0+2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + setCursor = true + } + c.columnsView.Frame = false + c.columnsView.BgColor = gocui.ColorGreen + c.columnsView.FgColor = gocui.ColorBlack + + c.view, err = g.SetView(ROUTING, x0-1, y0+1, x1+2, y1-1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + setCursor = true + } + c.view.Frame = false + c.view.Autoscroll = false + c.view.SelBgColor = gocui.ColorCyan + c.view.SelFgColor = gocui.ColorBlack + c.view.Highlight = true + if setCursor { + ox, oy := c.Origin() + err := c.SetOrigin(ox, oy) + if err != nil { + return err + } + + cx, cy := c.Cursor() + err = c.SetCursor(cx, cy) + if err != nil { + return err + } + } + + c.display() + + footer, err := g.SetView(ROUTING_FOOTER, x0-1, y1-2, x1+2, y1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + } + footer.Frame = false + footer.BgColor = gocui.ColorCyan + footer.FgColor = gocui.ColorBlack + footer.Clear() + blackBg := color.Black(color.Background) + fmt.Fprintln(footer, fmt.Sprintf("%s%s %s%s %s%s", + blackBg("F1"), "Help", + blackBg("F2"), "Menu", + blackBg("F10"), "Quit", + )) + return nil +} + +func (c *Routing) display() { + c.columnsView.Clear() + var buffer bytes.Buffer + currentColumnIndex := c.currentColumnIndex() + for i := range c.columns { + if currentColumnIndex == i { + buffer.WriteString(color.Cyan(color.Background)(c.columns[i].name)) + buffer.WriteString(" ") + continue + } + buffer.WriteString(c.columns[i].name) + buffer.WriteString(" ") + } + fmt.Fprintln(c.columnsView, buffer.String()) + + c.view.Clear() + + _, height := c.view.Size() + numEvents := len(c.routingEvents.Log) + + j := 0 + if height < numEvents { + j = numEvents - height + } + for ; j < numEvents; j++ { + var item = c.routingEvents.Log[j] + var buffer bytes.Buffer + for i := range c.columns { + var opt color.Option + if currentColumnIndex == i { + opt = color.Bold + } + buffer.WriteString(c.columns[i].display(item, opt)) + buffer.WriteString(" ") + } + fmt.Fprintln(c.view, buffer.String()) + } +} + +func NewRouting(cfg *config.View, routingEvents *models.RoutingLog, channels *models.Channels) *Routing { + routing := &Routing{ + cfg: cfg, + routingEvents: routingEvents, + } + + printer := message.NewPrinter(language.English) + + columns := DefaultRoutingColumns + if cfg != nil && len(cfg.Columns) != 0 { + columns = cfg.Columns + } + + routing.columns = make([]routingColumn, len(columns)) + + for i := range columns { + switch columns[i] { + case "DIR": + routing.columns[i] = routingColumn{ + width: 4, + name: fmt.Sprintf("%-4s", columns[i]), + display: rdirection, + } + case "STATUS": + routing.columns[i] = routingColumn{ + width: 8, + name: fmt.Sprintf("%-8s", columns[i]), + display: rstatus, + } + case "IN_ALIAS": + routing.columns[i] = routingColumn{ + width: 25, + name: fmt.Sprintf("%-25s", columns[i]), + display: ralias(channels, false), + } + case "IN_CHANNEL": + routing.columns[i] = routingColumn{ + width: 19, + name: fmt.Sprintf("%19s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + if c.IncomingChannelId == 0 { + return fmt.Sprintf("%19s", "") + } + return color.White(opts...)(fmt.Sprintf("%19d", c.IncomingChannelId)) + }, + } + case "IN_TIMELOCK": + routing.columns[i] = routingColumn{ + width: 10, + name: fmt.Sprintf("%10s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + if c.IncomingTimelock == 0 { + return fmt.Sprintf("%10s", "") + } + return color.White(opts...)(fmt.Sprintf("%10d", c.IncomingTimelock)) + }, + } + case "IN_HTLC": + routing.columns[i] = routingColumn{ + width: 10, + name: fmt.Sprintf("%10s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + if c.IncomingHtlcId == 0 { + return fmt.Sprintf("%10s", "") + } + return color.White(opts...)(fmt.Sprintf("%10d", c.IncomingHtlcId)) + }, + } + case "OUT_ALIAS": + routing.columns[i] = routingColumn{ + width: 25, + name: fmt.Sprintf("%-25s", columns[i]), + display: ralias(channels, true), + } + case "OUT_CHANNEL": + routing.columns[i] = routingColumn{ + width: 19, + name: fmt.Sprintf("%19s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + if c.OutgoingChannelId == 0 { + return fmt.Sprintf("%19s", "") + } + return color.White(opts...)(fmt.Sprintf("%19d", c.OutgoingChannelId)) + }, + } + case "OUT_TIMELOCK": + routing.columns[i] = routingColumn{ + width: 10, + name: fmt.Sprintf("%10s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + if c.OutgoingTimelock == 0 { + return fmt.Sprintf("%10s", "") + } + return color.White(opts...)(fmt.Sprintf("%10d", c.OutgoingTimelock)) + }, + } + case "OUT_HTLC": + routing.columns[i] = routingColumn{ + width: 10, + name: fmt.Sprintf("%10s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + if c.OutgoingHtlcId == 0 { + return fmt.Sprintf("%10s", "") + } + return color.White(opts...)(fmt.Sprintf("%10d", c.OutgoingHtlcId)) + }, + } + case "AMOUNT": + routing.columns[i] = routingColumn{ + width: 12, + name: fmt.Sprintf("%12s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + return color.Yellow(opts...)(printer.Sprintf("%12d", c.AmountMsat/1000)) + }, + } + case "FEE": + routing.columns[i] = routingColumn{ + width: 8, + name: fmt.Sprintf("%8s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + return color.Yellow(opts...)(printer.Sprintf("%8d", c.FeeMsat/1000)) + }, + } + case "LAST UPDATE": + routing.columns[i] = routingColumn{ + width: 15, + name: fmt.Sprintf("%-15s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + return color.Cyan(opts...)( + fmt.Sprintf("%15s", c.LastUpdate.Format("15:04:05 Jan _2")), + ) + }, + } + case "DETAIL": + routing.columns[i] = routingColumn{ + width: 80, + name: fmt.Sprintf("%-80s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + return color.Cyan(opts...)(fmt.Sprintf("%-80s", c.FailureDetail)) + }, + } + default: + routing.columns[i] = routingColumn{ + width: 10, + name: fmt.Sprintf("%-10s", columns[i]), + display: func(c *netmodels.RoutingEvent, opts ...color.Option) string { + return fmt.Sprintf("%-10s", "") + }, + } + } + } + + return routing +} + +func rstatus(c *netmodels.RoutingEvent, opts ...color.Option) string { + switch c.Status { + case netmodels.RoutingStatusActive: + return color.Yellow(opts...)(fmt.Sprintf("%-8s", "active")) + case netmodels.RoutingStatusSettled: + return color.Green(opts...)(fmt.Sprintf("%-8s", "settled")) + case netmodels.RoutingStatusFailed: + return color.Red(opts...)(fmt.Sprintf("%-8s", "failed")) + case netmodels.RoutingStatusLinkFailed: + return color.Red(opts...)(fmt.Sprintf("%-8s", "linkfail")) + } + return "" +} + +func rdirection(c *netmodels.RoutingEvent, opts ...color.Option) string { + switch c.Direction { + case netmodels.RoutingSend: + return color.White(opts...)(fmt.Sprintf("%-4s", "send")) + case netmodels.RoutingReceive: + return color.White(opts...)(fmt.Sprintf("%-4s", "recv")) + case netmodels.RoutingForward: + return color.White(opts...)(fmt.Sprintf("%-4s", "forw")) + } + return " " +} + +func ralias(channels *models.Channels, out bool) func(*netmodels.RoutingEvent, ...color.Option) string { + return func(c *netmodels.RoutingEvent, opts ...color.Option) string { + id := c.IncomingChannelId + if out { + id = c.OutgoingChannelId + } + + if id == 0 { + return color.White(opts...)(fmt.Sprintf("%-25s", "")) + } + + var alias string + for _, ch := range channels.List() { + if ch.ID == id { + if ch.Node == nil || ch.Node.Alias == "" { + alias = ch.RemotePubKey[:24] + } else if len(ch.Node.Alias) > 25 { + alias = ch.Node.Alias[:24] + } else { + alias = ch.Node.Alias + } + break + } + } + return color.White(opts...)(fmt.Sprintf("%-25s", alias)) + } +} diff --git a/ui/views/views.go b/ui/views/views.go index cde8fe0..2c13b3a 100644 --- a/ui/views/views.go +++ b/ui/views/views.go @@ -28,6 +28,7 @@ type Views struct { Channel *Channel Transactions *Transactions Transaction *Transaction + Routing *Routing } func (v Views) Get(vi *gocui.View) View { @@ -47,6 +48,8 @@ func (v Views) Get(vi *gocui.View) View { return v.Transactions.Wrap(vi) case TRANSACTION: return v.Transaction.Wrap(vi) + case ROUTING: + return v.Routing.Wrap(vi) default: return nil } @@ -106,6 +109,7 @@ func New(cfg config.Views, m *models.Models) *Views { Channel: NewChannel(m.Channels), Transactions: NewTransactions(cfg.Transactions, m.Transactions), Transaction: NewTransaction(m.Transactions), + Routing: NewRouting(cfg.Routing, m.RoutingLog, m.Channels), Main: main, } } From 2e9cced9c3554657e719d7c9ae1b00424acd13e9 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Sun, 9 May 2021 23:40:17 +0200 Subject: [PATCH 3/3] increase default pool capacity Ignore lower values, lntop won't start with pool_capacity <= 3 due to router rpc handle. --- README.md | 2 +- config/default.go | 2 +- network/backend/lnd/lnd.go | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 524c838..d33df39 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ macaroon = "/root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" macaroon_timeout = 60 max_msg_recv_size = 52428800 conn_timeout = 1000000 -pool_capacity = 3 +pool_capacity = 4 [views] # views.channels is the view displaying channel list. diff --git a/config/default.go b/config/default.go index b012504..1602172 100644 --- a/config/default.go +++ b/config/default.go @@ -105,7 +105,7 @@ func NewDefault() *Config { MacaroonTimeOut: 60, MaxMsgRecvSize: 52428800, ConnTimeout: 1000000, - PoolCapacity: 3, + PoolCapacity: 4, }, } } diff --git a/network/backend/lnd/lnd.go b/network/backend/lnd/lnd.go index a33b457..9ce1619 100644 --- a/network/backend/lnd/lnd.go +++ b/network/backend/lnd/lnd.go @@ -21,6 +21,7 @@ import ( const ( lndDefaultInvoiceExpiry = 3600 + lndMinPoolCapacity = 4 ) type Client struct { @@ -479,6 +480,10 @@ func New(c *config.Network, logger logging.Logger) (*Backend, error) { logger: logger.With(logging.String("name", c.Name)), } + if c.PoolCapacity < lndMinPoolCapacity { + c.PoolCapacity = lndMinPoolCapacity + logger.Info("pool_capacity too small, ignoring") + } backend.pool, err = pool.New(backend.NewClientConn, c.PoolCapacity, time.Duration(c.ConnTimeout)) if err != nil { return nil, err