diff --git a/README.md b/README.md index 4984b57..dd897d2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ go get -u github.com/gchaincl/mempool ``` # TODO -- [ ] Transaction Tracking (by Tx ID) +- [x] Transaction Tracking (by Tx ID) (using 'f' key) - [x] Block details on click - [ ] Graphs - [ ] Tx weight per second progress bar diff --git a/client/client.go b/client/client.go index 6d22907..8e61f36 100644 --- a/client/client.go +++ b/client/client.go @@ -44,6 +44,17 @@ type ProjectedBlock struct { HasMyTx bool `json:"hasMytx"` } +type TrackTx struct { + Tracking bool `json:"tracking"` + BlockHeight int `json:"blockHeight"` + Message string `json:"message"` + TX struct { + Status struct { + Confirmed bool + } + } `json:"tx"` +} + type Response struct { MempoolInfo *MempoolInfo `json:"mempoolInfo"` @@ -51,6 +62,7 @@ type Response struct { Blocks []Block `json:"blocks"` ProjectedBlocks []ProjectedBlock `json:"projectedBlocks"` + TrackTx TrackTx `json:"track-tx"` TxPerSecond float64 `json:"txPerSecond"` VBytesPerSecond int `json:"vBytesPerSecond"` @@ -70,12 +82,6 @@ func New() (*Client, error) { if err != nil { return nil, err } - - if err := conn.WriteMessage(websocket.TextMessage, []byte( - `{"action":"want","data":["stats","blocks","projected-blocks"]}`, - )); err != nil { - return nil, err - } return &Client{conn: conn}, nil } @@ -87,6 +93,18 @@ func (c *Client) Read() (*Response, error) { return &resp, nil } +func (c *Client) Want() error { + return c.conn.WriteMessage(websocket.TextMessage, []byte( + `{"action":"want","data":["stats","blocks","projected-blocks"]}`, + )) +} + +func (c *Client) Track(txId string) error { + return c.conn.WriteMessage(websocket.TextMessage, []byte( + fmt.Sprintf(`{"action":"track-tx","txId":"%s"}`, txId), + )) +} + type Fees []struct { FPV float64 `json:"fpv"` } diff --git a/main.go b/main.go index cb1e544..84ea367 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "log" - "github.com/gchaincl/mempool/client" "github.com/gchaincl/mempool/ui" ) @@ -14,22 +13,6 @@ func main() { } defer gui.Close() - go func() { - c, err := client.New() - if err != nil { - log.Fatal(err) - } - - for { - resp, err := c.Read() - if err != nil { - log.Fatal(err) - } - - gui.Render(resp) - } - }() - if err := gui.Loop(); err != nil { log.Fatal(err) } diff --git a/ui/tx_search.go b/ui/tx_search.go new file mode 100644 index 0000000..1bf459b --- /dev/null +++ b/ui/tx_search.go @@ -0,0 +1,88 @@ +package ui + +import ( + "fmt" + + "github.com/jroimartin/gocui" +) + +type TXSearch struct { + gui *gocui.Gui + + opened bool + txid string + cb func(string) error +} + +func NewTXSearch(gui *gocui.Gui) *TXSearch { + ts := &TXSearch{gui: gui} + return ts +} + +func (s *TXSearch) Callback(fn func(txId string) error) { + s.cb = fn +} + +func (s *TXSearch) SetKeybinding() { + s.gui.SetKeybinding("", 'f', gocui.ModNone, func(*gocui.Gui, *gocui.View) error { + s.gui.DeleteKeybinding("", 'f', gocui.ModNone) + s.Open() + return nil + }) +} + +func (s *TXSearch) Layout(g *gocui.Gui) error { + name := "tx_search" + if !s.opened { + g.Cursor = false + g.DeleteView(name) + return nil + } + + g.Cursor = true + x, y := g.Size() + v, err := g.SetView(name, x/2-35, y/2-1, x/2+35, y/2+1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Track transaction (txid)" + v.Editable = true + g.SetCurrentView(name) + v.Editor = gocui.EditorFunc(s.editFn) + v.Autoscroll = false + fmt.Fprintf(v, "%s", s.txid) + v.SetCursor(len(s.txid), 0) + + g.SetKeybinding(v.Name(), gocui.KeyEsc, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { + s.Close() + return nil + }) + } + + return nil +} + +func (s *TXSearch) editFn(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { + switch key { + case gocui.KeyEnter: + if id := s.txid; id != "" { + s.cb(id) + } + s.Close() + case gocui.KeyArrowDown, gocui.KeyArrowUp: + return + } + + gocui.DefaultEditor.Edit(v, key, ch, mod) + s.txid, _ = v.Line(0) +} + +func (s *TXSearch) Open() { + s.opened = true +} + +func (s *TXSearch) Close() { + s.SetKeybinding() + s.opened = false +} diff --git a/ui/ui.go b/ui/ui.go index 9782347..82d4220 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "log" "strconv" "strings" @@ -20,12 +21,15 @@ type state struct { projected []client.ProjectedBlock vBytesPerSecond int info *client.MempoolInfo + tracking *client.TrackTx } type UI struct { - gui *gocui.Gui - fd *FeeDistribution - state state + client *client.Client + gui *gocui.Gui + fd *FeeDistribution + ts *TXSearch + state state } func New() (*UI, error) { @@ -36,14 +40,40 @@ func New() (*UI, error) { ui := &UI{gui: gui} ui.fd = NewFeeDistribution(gui) - gui.SetManager(ui, ui.fd) + ui.ts = NewTXSearch(gui) + gui.SetManager(ui, ui.fd, ui.ts) + gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit) - gui.SetKeybinding("", 'q', 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 } @@ -87,6 +117,9 @@ func (ui *UI) Render(resp *client.Response) { 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()) @@ -108,6 +141,8 @@ func (ui *UI) Layout(g *gocui.Gui) error { // and the blockchain in the bottom vertical := BLOCK_WIDTH*6 > x + track := ui.state.tracking + // draw projected blocks (mempool) for i, _ := range ui.state.projected { name := fmt.Sprintf("projected-block-%d", i) @@ -131,6 +166,14 @@ func (ui *UI) Layout(g *gocui.Gui) error { } 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() @@ -166,9 +209,17 @@ func (ui *UI) Layout(g *gocui.Gui) error { } 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