mempool-cli/ui/ui.go
Gustavo Chain 1ac079b082
Some checks failed
release / goreleaser (push) Has been cancelled
Add halving banner
2020-04-10 19:20:14 +02:00

373 lines
7.4 KiB
Go

package ui
import (
"fmt"
"log"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jroimartin/gocui"
"github.com/mempool/mempool-cli/client"
)
const (
BLOCK_WIDTH = 22
NEXT_HALVING = 630_000
)
type state struct {
loaded bool
blocks []client.Block
mempool []client.MempoolBlock
vBytesPerSecond int
info *client.MempoolInfo
tracking *client.TrackTx
}
type UI struct {
client *client.Client
gui *gocui.Gui
fd *FeeDistribution
ts *TXSearch
state state
}
func New() (*UI, error) {
gui, err := gocui.NewGui(gocui.Output256)
if err != nil {
return nil, err
}
ui := &UI{gui: gui}
ui.fd = NewFeeDistribution(gui)
ui.ts = NewTXSearch(gui)
gui.SetManager(ui, ui.fd, ui.ts)
gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit)
ui.ts.SetKeybinding()
gui.Mouse = true
gui.Highlight = true
gui.InputEsc = true
gui.SelFgColor = gocui.ColorWhite
go func() {
c, err := client.New()
if err != nil {
log.Fatal(err)
}
if err := c.Want(); err != nil {
log.Fatal(err)
}
ui.client = c
ui.ts.Callback(func(txId string) error {
return c.Track(txId)
})
for {
resp, err := c.Read()
if err != nil {
log.Fatal(err)
}
ui.Render(resp)
}
}()
return ui, nil
}
func quit(*gocui.Gui, *gocui.View) error { return gocui.ErrQuit }
func (ui *UI) Close() { ui.gui.Close() }
func (ui *UI) Loop() error {
err := ui.gui.MainLoop()
// Mask ErrQuit
if err == gocui.ErrQuit {
return nil
}
return err
}
func (ui *UI) Render(resp *client.Response) {
ui.state.loaded = true
ui.state.vBytesPerSecond = resp.VBytesPerSecond
nBlocks := len(resp.Blocks)
blocks := make([]client.Block, nBlocks)
for i := 0; i < nBlocks; i++ {
blocks[i] = resp.Blocks[len(resp.Blocks)-1-i]
}
if bs := blocks; len(bs) != 0 {
ui.state.blocks = bs
}
if bs := resp.MempoolBlocks; len(bs) != 0 {
ui.state.mempool = bs
}
if b := resp.Block; b != nil {
ui.state.blocks = append([]client.Block{*b}, ui.state.blocks...)
}
if info := resp.MempoolInfo; info != nil {
ui.state.info = info
}
// Update tracking info
ui.state.tracking = &resp.TrackTx
// delete all the views
for _, v := range ui.gui.Views() {
ui.gui.DeleteView(v.Name())
}
ui.gui.Update(ui.Layout)
}
func (ui *UI) Layout(g *gocui.Gui) error {
x, y := g.Size()
if !ui.state.loaded {
return ui.loading(g, x, y)
}
g.DeleteView("loading")
// vertical layout is used if 8 blocks don't fit on the screen
// when in vertical layout the mempool is shown in the top
// and the blockchain in the bottom
vertical := BLOCK_WIDTH*6 > x
track := ui.state.tracking
// draw mempool blocks
for i, _ := range ui.state.mempool {
name := fmt.Sprintf("mempool-block-%d", i)
var x0, x1, y0, y1 int
if vertical {
x0 = x - (BLOCK_WIDTH+2)*(i+1)
x1 = x0 + BLOCK_WIDTH
y0 = (y / 2) - 12
y1 = (y / 2) - 2
} else {
x0 = x/2 - BLOCK_WIDTH*(i+1)
x1 = x0 + BLOCK_WIDTH - 2
y0 = (y / 2) - 5
y1 = (y / 2) + 5
}
v, err := g.SetView(name, x0, y0, x1, y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorBlack
g.SetKeybinding(v.Name(), gocui.MouseLeft, gocui.ModNone, ui.onBlockClick)
if track.Tracking && !track.TX.Status.Confirmed {
if track.BlockHeight == i {
v.SelBgColor = gocui.ColorRed
v.SelFgColor = gocui.ColorRed
g.SetCurrentView(v.Name())
}
}
}
v.Clear()
if _, err := v.Write(ui.printMempoolBlock(i, x1-x0, y1-y0)); err != nil {
return err
}
}
if err := ui.separator(g, x, y, vertical); err != nil {
return err
}
// draw blockchain blocks
for i, block := range ui.state.blocks {
name := fmt.Sprintf("block-%d", block.Height)
var x0, x1, y0, y1 int
if vertical {
x0 = x - (BLOCK_WIDTH+2)*(i+1)
x1 = x0 + BLOCK_WIDTH
y0 = (y / 2) + 2
y1 = (y / 2) + 12
} else {
x0 = (x / 2) + (BLOCK_WIDTH*i + 1) + 1
x1 = x0 + BLOCK_WIDTH - 2
y0 = (y / 2) - 5
y1 = (y / 2) + 5
}
v, err := g.SetView(name, x0, y0, x1, y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorBlack
g.SetKeybinding(v.Name(), gocui.MouseLeft, gocui.ModNone, ui.onBlockClick)
}
v.Title = fmt.Sprintf("#%d", block.Height)
if track.Tracking && track.TX.Status.Confirmed {
if track.BlockHeight == block.Height {
v.SelBgColor = gocui.ColorRed
v.SelFgColor = gocui.ColorRed
g.SetCurrentView(v.Name())
}
}
v.Clear()
if _, err := v.Write(ui.printBlock(i, x1-x0, y1-y0)); err != nil {
return err
}
}
if err := ui.info(g, x, y); err != nil {
return err
}
// halving
if err := ui.halvingBanner(); err != nil {
return err
}
return nil
}
func (ui *UI) loading(g *gocui.Gui, x, y int) error {
v, err := g.SetView("loading", x/2-10, y/2-1, x/2+10, y/2+1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.Clear()
fmt.Fprintf(v, "Loading blocks ...")
return nil
}
func (ui *UI) printMempoolBlock(n int, x, y int) []byte {
b := ui.state.mempool[n]
return MempoolBlock(b).Print(n, x, y)
}
func (ui *UI) printBlock(n int, x, y int) []byte {
b := ui.state.blocks[n]
return Block(b).Print(n, x, y)
}
func (ui *UI) separator(g *gocui.Gui, x, y int, vertical bool) error {
var x0, x1, y0, y1 int
if vertical {
x0, x1 = 0, x
y0, y1 = y/2-1, y/2+1
} else {
x0, x1 = x/2-1, x/2+1
y0, y1 = 0, y
}
v, err := g.SetView("separator", x0, y0, x1, y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.Wrap = true
}
v.Clear()
if vertical {
fmt.Fprintf(v, strings.Repeat("-", x))
} else {
fmt.Fprintf(v, strings.Repeat("|", y))
}
return nil
}
func (ui *UI) info(g *gocui.Gui, x, y int) error {
var (
white = color.New(color.FgWhite).SprintfFunc()
red = color.New(color.FgRed).SprintfFunc()
blue = color.New(color.FgBlue).SprintfFunc()
)
v, err := g.SetView("info", 0, y-2, x, y)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.BgColor = gocui.ColorBlack
}
v.Clear()
info := ui.state.info
if info == nil {
return nil
}
var mSize int
for _, b := range ui.state.mempool {
mSize += b.BlockSize
}
// Compute the total number of blocks on the mempool
// We use the total BlockWeight / 4mm
var w float64
for _, b := range ui.state.mempool {
w += float64(b.BlockWeight)
}
fmt.Fprintf(v, "%s %s, %s %s, %s %s",
red("Unconfirmed Txs: "), white("%d", info.Size),
blue("Mempool size"), white("%s (%d blocks)", fmtSize(mSize), ceil(w/4_000_000)),
blue("Tx weight per second"), red("%d vBytes/s", ui.state.vBytesPerSecond),
)
return nil
}
func (ui *UI) halvingBanner() error {
blocks := ui.state.blocks
if len(blocks) == 0 {
return nil
}
height := blocks[0].Height
if height >= NEXT_HALVING {
return nil
}
in := NEXT_HALVING - height
msg := fmt.Sprintf("Quantitative Hardening in %d blocks", in)
v, err := ui.gui.SetView("halving", 1, 1, len(msg)+2, 3)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
_, err = fmt.Fprintf(v, msg)
return err
}
func (ui *UI) onBlockClick(g *gocui.Gui, v *gocui.View) error {
name := v.Name()
if strings.HasPrefix(name, "mempool-block-") {
id := strings.TrimPrefix(name, "mempool-block-")
n, _ := strconv.Atoi(id)
return ui.fd.FetchProjection(n)
}
if strings.HasPrefix(name, "block-") {
id := strings.TrimPrefix(name, "block-")
n, _ := strconv.Atoi(id)
return ui.fd.FetchBlock(n)
}
return nil
}