From 4f099e374601f68b0e4c5ae0a1727abfd0a7802e Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 8 Nov 2022 18:51:13 -0500 Subject: [PATCH] Forwarding history UI changes --- ui/controller.go | 15 ++ ui/models/fwdinghist.go | 72 +++++++ ui/models/models.go | 35 ++++ ui/views/fwdinghist.go | 418 ++++++++++++++++++++++++++++++++++++++++ ui/views/menu.go | 3 + ui/views/views.go | 4 + 6 files changed, 547 insertions(+) create mode 100644 ui/models/fwdinghist.go create mode 100644 ui/views/fwdinghist.go diff --git a/ui/controller.go b/ui/controller.go index 4df3b6d..f3251d3 100644 --- a/ui/controller.go +++ b/ui/controller.go @@ -227,6 +227,8 @@ func (c *controller) Order(order models.Order) func(*gocui.Gui, *gocui.View) err c.views.Channels.Sort("", order) case views.TRANSACTIONS: c.views.Transactions.Sort("", order) + case views.FWDINGHIST: + c.views.FwdingHist.Sort("", order) } return nil } @@ -293,6 +295,19 @@ func (c *controller) OnEnter(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } + case views.FWDINGHIST: + err := c.views.Main.Delete(g) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + c.models.RefreshForwardingHistory(ctx) + c.views.Main = c.views.FwdingHist + err = c.views.FwdingHist.Set(g, 11, 6, maxX-1, maxY) + if err != nil { + return err + } } case views.TRANSACTIONS: diff --git a/ui/models/fwdinghist.go b/ui/models/fwdinghist.go new file mode 100644 index 0000000..78b42ee --- /dev/null +++ b/ui/models/fwdinghist.go @@ -0,0 +1,72 @@ +package models + +import ( + "sort" + "sync" + + "github.com/edouardparis/lntop/network/models" +) + +type FwdinghistSort func(*models.ForwardingEvent, *models.ForwardingEvent) bool + +type FwdingHist struct { + StartTime string + MaxNumEvents uint32 + current *models.ForwardingEvent + list []*models.ForwardingEvent + sort FwdinghistSort + mu sync.RWMutex +} + +func (t *FwdingHist) Current() *models.ForwardingEvent { + return t.current +} + +func (t *FwdingHist) SetCurrent(index int) { + t.current = t.Get(index) +} + +func (t *FwdingHist) List() []*models.ForwardingEvent { + return t.list +} + +func (t *FwdingHist) Len() int { + return len(t.list) +} + +func (t *FwdingHist) Clear() { + t.list = []*models.ForwardingEvent{} +} + +func (t *FwdingHist) Swap(i, j int) { + t.list[i], t.list[j] = t.list[j], t.list[i] +} + +func (t *FwdingHist) Less(i, j int) bool { + return t.sort(t.list[i], t.list[j]) +} + +func (t *FwdingHist) Sort(s FwdinghistSort) { + if s == nil { + return + } + t.sort = s + sort.Sort(t) +} + +func (t *FwdingHist) Get(index int) *models.ForwardingEvent { + if index < 0 || index > len(t.list)-1 { + return nil + } + + return t.list[index] +} + +func (t *FwdingHist) Update(events []*models.ForwardingEvent) { + t.mu.Lock() + defer t.mu.Unlock() + t.Clear() + for _, event := range events { + t.list = append(t.list, event) + } +} diff --git a/ui/models/models.go b/ui/models/models.go index e606f73..8c046a3 100644 --- a/ui/models/models.go +++ b/ui/models/models.go @@ -2,6 +2,7 @@ package models import ( "context" + "strconv" "github.com/edouardparis/lntop/app" "github.com/edouardparis/lntop/logging" @@ -19,9 +20,27 @@ type Models struct { ChannelsBalance *ChannelsBalance Transactions *Transactions RoutingLog *RoutingLog + FwdingHist *FwdingHist } func New(app *app.App) *Models { + fwdingHist := FwdingHist{} + startTime := app.Config.Views.FwdingHist.Options.GetOption("START_TIME", "start_time") + maxNumEvents := app.Config.Views.FwdingHist.Options.GetOption("MAX_NUM_EVENTS", "max_num_events") + + if startTime != "" { + fwdingHist.StartTime = startTime + } + + if maxNumEvents != "" { + max, err := strconv.ParseUint(maxNumEvents, 10, 32) + if err != nil { + app.Logger.Info("Couldn't parse the maximum number of forwarding events.") + } else { + fwdingHist.MaxNumEvents = uint32(max) + } + } + return &Models{ logger: app.Logger.With(logging.String("logger", "models")), network: app.Network, @@ -31,6 +50,7 @@ func New(app *app.App) *Models { ChannelsBalance: &ChannelsBalance{}, Transactions: &Transactions{}, RoutingLog: &RoutingLog{}, + FwdingHist: &fwdingHist, } } @@ -47,6 +67,21 @@ func (m *Models) RefreshInfo(ctx context.Context) error { return nil } +func (m *Models) RefreshForwardingHistory(ctx context.Context) error { +<<<<<<< Updated upstream + forwardingEvents, err := m.network.GetForwardingHistory(ctx) +======= + forwardingEvents, err := m.network.GetForwardingHistory(ctx, m.FwdingHist.StartTime, m.FwdingHist.MaxNumEvents) +>>>>>>> Stashed changes + if err != nil { + return err + } + + m.FwdingHist.Update(forwardingEvents) + + return nil +} + func (m *Models) RefreshChannels(ctx context.Context) error { channels, err := m.network.ListChannels(ctx, options.WithChannelPending) if err != nil { diff --git a/ui/views/fwdinghist.go b/ui/views/fwdinghist.go new file mode 100644 index 0000000..65fc933 --- /dev/null +++ b/ui/views/fwdinghist.go @@ -0,0 +1,418 @@ +package views + +import ( + "bytes" + "fmt" + + "github.com/awesome-gocui/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 ( + FWDINGHIST = "fwdinghist" + FWDINGHIST_COLUMNS = "fwdinghist_columns" + FWDINGHIST_FOOTER = "fwdinghist_footer" +) + +var DefaultFwdinghistColumns = []string{ + "ALIAS_IN", + "ALIAS_OUT", + "AMT_IN", + "AMT_OUT", + "FEE", + "TIMESTAMP_NS", + "CHAN_ID_IN", + "CHAN_ID_OUT", +} + +type FwdingHist struct { + cfg *config.View + + columns []fwdinghistColumn + columnHeadersView *gocui.View + view *gocui.View + fwdinghist *models.FwdingHist + + ox, oy int + cx, cy int +} + +type fwdinghistColumn struct { + name string + width int + sorted bool + sort func(models.Order) models.FwdinghistSort + display func(*netmodels.ForwardingEvent, ...color.Option) string +} + +func (c FwdingHist) Index() int { + _, oy := c.view.Origin() + _, cy := c.view.Cursor() + return cy + oy +} + +func (c FwdingHist) Name() string { + return FWDINGHIST +} + +func (c *FwdingHist) Wrap(v *gocui.View) View { + c.view = v + return c +} + +func (c FwdingHist) 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 FwdingHist) Origin() (int, int) { + return c.ox, c.oy +} + +func (c FwdingHist) Cursor() (int, int) { + return c.cx, c.cy +} + +func (c *FwdingHist) SetCursor(cx, cy int) error { + if err := cursorCompat(c.columnHeadersView, cx, 0); err != nil { + return err + } + err := c.columnHeadersView.SetCursor(cx, 0) + if err != nil { + return err + } + + if err := cursorCompat(c.view, cx, cy); 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 *FwdingHist) SetOrigin(ox, oy int) error { + err := c.columnHeadersView.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 *FwdingHist) Speed() (int, int, int, int) { + current := c.currentColumnIndex() + up := 0 + down := 0 + if c.Index() > 0 { + up = 1 + } + if c.Index() < c.fwdinghist.Len()-1 { + 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 *FwdingHist) Limits() (pageSize int, fullSize int) { + _, pageSize = c.view.Size() + fullSize = c.fwdinghist.Len() + return +} + +func (c *FwdingHist) Sort(column string, order models.Order) { + if column == "" { + index := c.currentColumnIndex() + if index >= len(c.columns) { + return + } + col := c.columns[index] + if col.sort == nil { + return + } + + c.fwdinghist.Sort(col.sort(order)) + for i := range c.columns { + c.columns[i].sorted = (i == index) + } + } +} + +func (c FwdingHist) Delete(g *gocui.Gui) error { + err := g.DeleteView(FWDINGHIST_COLUMNS) + if err != nil { + return err + } + + err = g.DeleteView(FWDINGHIST) + if err != nil { + return err + } + + return g.DeleteView(FWDINGHIST_FOOTER) +} + +func (c *FwdingHist) Set(g *gocui.Gui, x0, y0, x1, y1 int) error { + var err error + setCursor := false + c.columnHeadersView, err = g.SetView(FWDINGHIST_COLUMNS, x0-1, y0, x1+2, y0+2, 0) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + setCursor = true + } + c.columnHeadersView.Frame = false + c.columnHeadersView.BgColor = gocui.ColorGreen + c.columnHeadersView.FgColor = gocui.ColorBlack + + c.view, err = g.SetView(FWDINGHIST, x0-1, y0+1, x1+2, y1-1, 0) + 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 | gocui.AttrDim + c.view.Highlight = true + c.display() + + 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 + } + } + + footer, err := g.SetView(FWDINGHIST_FOOTER, x0-1, y1-2, x1+2, y1, 0) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + } + footer.Frame = false + footer.BgColor = gocui.ColorCyan + footer.FgColor = gocui.ColorBlack + footer.Rewind() + blackBg := color.Black(color.Background) + fmt.Fprintln(footer, fmt.Sprintf("%s%s %s%s %s%s", + blackBg("F2"), "Menu", + blackBg("Enter"), "FwdingHist", + blackBg("F10"), "Quit", + )) + return nil +} + +func (c *FwdingHist) display() { + c.columnHeadersView.Rewind() + var buffer bytes.Buffer + current := c.currentColumnIndex() + for i := range c.columns { + if current == i { + buffer.WriteString(color.Cyan(color.Background)(c.columns[i].name)) + buffer.WriteString(" ") + continue + } else if c.columns[i].sorted { + buffer.WriteString(color.Magenta(color.Background)(c.columns[i].name)) + buffer.WriteString(" ") + continue + } + buffer.WriteString(c.columns[i].name) + buffer.WriteString(" ") + } + fmt.Fprintln(c.columnHeadersView, buffer.String()) + + c.view.Rewind() + for _, item := range c.fwdinghist.List() { + var buffer bytes.Buffer + for i := range c.columns { + var opt color.Option + if current == i { + opt = color.Bold + } + buffer.WriteString(c.columns[i].display(item, opt)) + buffer.WriteString(" ") + } + fmt.Fprintln(c.view, buffer.String()) + } +} + +func NewFwdingHist(cfg *config.View, hist *models.FwdingHist) *FwdingHist { + fwdinghist := &FwdingHist{ + cfg: cfg, + fwdinghist: hist, + } + + printer := message.NewPrinter(language.English) + + columns := DefaultFwdinghistColumns + if cfg != nil && len(cfg.Columns) != 0 { + columns = cfg.Columns + } + + fwdinghist.columns = make([]fwdinghistColumn, len(columns)) + + for i := range columns { + switch columns[i] { + case "ALIAS_IN": + fwdinghist.columns[i] = fwdinghistColumn{ + width: 30, + name: fmt.Sprintf("%30s", columns[i]), + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.StringSort(e1.PeerAliasIn, e2.PeerAliasOut, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(fmt.Sprintf("%30s", e.PeerAliasIn)) + }, + } + case "ALIAS_OUT": + fwdinghist.columns[i] = fwdinghistColumn{ + width: 30, + name: fmt.Sprintf("%30s", columns[i]), + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.StringSort(e1.PeerAliasOut, e2.PeerAliasOut, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(fmt.Sprintf("%30s", e.PeerAliasOut)) + }, + } + case "CHAN_ID_IN": + fwdinghist.columns[i] = fwdinghistColumn{ + width: 19, + name: fmt.Sprintf("%19s", columns[i]), + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.UInt64Sort(e1.ChanIdIn, e2.ChanIdIn, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(fmt.Sprintf("%19d", e.ChanIdIn)) + }, + } + case "CHAN_ID_OUT": + fwdinghist.columns[i] = fwdinghistColumn{ + width: 19, + name: fmt.Sprintf("%19s", columns[i]), + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.UInt64Sort(e1.ChanIdOut, e2.ChanIdOut, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(fmt.Sprintf("%19d", e.ChanIdOut)) + }, + } + case "AMT_IN": + fwdinghist.columns[i] = fwdinghistColumn{ + width: 12, + name: fmt.Sprintf("%12s", "RECEIVED"), + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.UInt64Sort(e1.AmtIn, e2.AmtIn, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(printer.Sprintf("%12d", e.AmtIn)) + }, + } + case "AMT_OUT": + fwdinghist.columns[i] = fwdinghistColumn{ + width: 12, + name: fmt.Sprintf("%12s", "SENT"), + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.UInt64Sort(e1.AmtOut, e2.AmtOut, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(printer.Sprintf("%12d", e.AmtOut)) + }, + } + case "FEE": + fwdinghist.columns[i] = fwdinghistColumn{ + name: fmt.Sprintf("%9s", "EARNED"), + width: 9, + sort: func(order models.Order) models.FwdinghistSort { + return func(e1, e2 *netmodels.ForwardingEvent) bool { + return models.UInt64Sort(e1.Fee, e2.Fee, order) + } + }, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return fee(e.Fee) + }, + } + case "TIMESTAMP_NS": + fwdinghist.columns[i] = fwdinghistColumn{ + name: fmt.Sprintf("%15s", "TIME"), + width: 20, + display: func(e *netmodels.ForwardingEvent, opts ...color.Option) string { + return color.White(opts...)(fmt.Sprintf("%20s", e.EventTime.Format("15:04:05 Jan _2"))) + }, + } + default: + fwdinghist.columns[i] = fwdinghistColumn{ + name: fmt.Sprintf("%-21s", columns[i]), + width: 21, + display: func(tx *netmodels.ForwardingEvent, opts ...color.Option) string { + return "column does not exist" + }, + } + } + + } + return fwdinghist +} + +func fee(fee uint64, opts ...color.Option) string { + if fee >= 0 && fee < 100 { + return color.Cyan(opts...)(fmt.Sprintf("%9d", fee)) + } else if fee >= 100 && fee < 999 { + return color.Green(opts...)(fmt.Sprintf("%9d", fee)) + } + + return color.Yellow(opts...)(fmt.Sprintf("%9d", fee)) +} diff --git a/ui/views/menu.go b/ui/views/menu.go index 3e32bbf..98d92e8 100644 --- a/ui/views/menu.go +++ b/ui/views/menu.go @@ -17,6 +17,7 @@ var menu = []string{ "CHANNEL", "TRANSAC", "ROUTING", + "FWDHIST", } type Menu struct { @@ -85,6 +86,8 @@ func (h Menu) Current() string { return TRANSACTIONS case "ROUTING": return ROUTING + case "FWDHIST": + return FWDINGHIST } } return "" diff --git a/ui/views/views.go b/ui/views/views.go index b1f93a2..d18b10c 100644 --- a/ui/views/views.go +++ b/ui/views/views.go @@ -31,6 +31,7 @@ type Views struct { Transactions *Transactions Transaction *Transaction Routing *Routing + FwdingHist *FwdingHist } func (v Views) Get(vi *gocui.View) View { @@ -50,6 +51,8 @@ func (v Views) Get(vi *gocui.View) View { return v.Transaction.Wrap(vi) case ROUTING: return v.Routing.Wrap(vi) + case FWDINGHIST: + return v.FwdingHist.Wrap(vi) default: return nil } @@ -106,6 +109,7 @@ func New(cfg config.Views, m *models.Models) *Views { Transactions: NewTransactions(cfg.Transactions, m.Transactions), Transaction: NewTransaction(m.Transactions), Routing: NewRouting(cfg.Routing, m.RoutingLog, m.Channels), + FwdingHist: NewFwdingHist(cfg.FwdingHist, m.FwdingHist), Main: main, } }