373 lines
7.4 KiB
Go
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
|
|
}
|