Merge pull request #22 from mmilata/routing-view

Add routing view
pull/24/head
Edouard 3 years ago committed by GitHub
commit c923d7ab57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.
@ -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.

@ -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{

@ -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 {

@ -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,
@ -87,7 +105,7 @@ func NewDefault() *Config {
MacaroonTimeOut: 60,
MaxMsgRecvSize: 52428800,
ConnTimeout: 1000000,
PoolCapacity: 3,
PoolCapacity: 4,
},
}
}

@ -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}
}

@ -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
}

@ -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"
@ -20,6 +21,7 @@ import (
const (
lndDefaultInvoiceExpiry = 3600
lndMinPoolCapacity = 4
)
type Client struct {
@ -31,6 +33,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 +161,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 +205,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)
}
@ -425,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

@ -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,
}
}

@ -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
}

@ -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
}

@ -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(),

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

@ -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
})
}

@ -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 ""

@ -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))
}
}

@ -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,
}
}

Loading…
Cancel
Save