From be7651f415fc43c056e165580fffbd3f3a047214 Mon Sep 17 00:00:00 2001 From: Gustavo Chain Date: Thu, 13 Feb 2020 18:31:51 +0100 Subject: [PATCH] initial implementation --- client/client.go | 73 +++++++++++++++ go.mod | 17 ++++ go.sum | 74 +++++++++++++++ main.go | 36 ++++++++ ui/ui.go | 228 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 428 insertions(+) create mode 100644 client/client.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 ui/ui.go diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..a1246e4 --- /dev/null +++ b/client/client.go @@ -0,0 +1,73 @@ +package client + +import "github.com/gorilla/websocket" + +type Block struct { + Hash string `json:"hash"` + Height int `json:"height"` + NTx int `json:"nTx"` + Size int `json:"size"` + Time int `json:"time"` + Weight int `json:"weight"` + Fees float64 `json:"fees"` + MinFee float64 `json:"minFee"` + MaxFee float64 `json:"maxFee"` + MedianFee float64 `json:"medianFee"` +} + +type Response struct { + MempoolInfo struct { + Size int `json:"size"` + Bytes int `json:"bytes"` + } `json:"mempoolInfo"` + + Block Block `json:"block"` + Blocks []Block `json:"blocks"` + + ProjectedBlocks []struct { + BlockSize int `json:"blockSize"` + BlockWeight int `json:"blockWeight"` + NTx int `json:"nTx"` + MinFee float64 `json:"minFee"` + MaxFee float64 `json:"maxFee"` + MinWeigthFee float64 `json:"minWeigthFee"` + MaxWeigthFee float64 `json:"maxWeigthFee"` + MedianFee float64 `json:"medianFee"` + Fees float64 `json:"fees"` + HasMyTx bool `json:"hasMytx"` + } `json:"projectedBlocks"` + + TxPerSecond float64 `json:"txPerSecond"` + VBytesPerSecond int `json:"vBytesPerSecond"` + Conversions struct { + BTC float64 `json:"BTC"` + USD float64 `json:"USD"` + } `json:"conversions"` +} + +type Client struct { + conn *websocket.Conn +} + +func New() (*Client, error) { + dialer := websocket.Dialer{} + conn, _, err := dialer.Dial("wss://mempool.space/ws", nil) + 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 +} + +func (c *Client) Read() (*Response, error) { + var resp Response + if err := c.conn.ReadJSON(&resp); err != nil { + return nil, err + } + return &resp, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c70b657 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/gchaincl/mempool + +go 1.13 + +require ( + github.com/fatih/color v1.9.0 + github.com/gizak/termui v3.1.0+incompatible + github.com/gizak/termui/v3 v3.1.0 + github.com/golang/protobuf v1.3.3 // indirect + github.com/gorilla/websocket v1.4.1 + github.com/jroimartin/gocui v0.4.0 + github.com/kr/pretty v0.2.0 + github.com/mattn/go-runewidth v0.0.8 // indirect + github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be // indirect + github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c + google.golang.org/grpc v1.27.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..67b455a --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/gizak/termui v3.1.0+incompatible h1:N3CFm+j087lanTxPpHOmQs0uS3s5I9TxoAFy6DqPqv8= +github.com/gizak/termui v3.1.0+incompatible/go.mod h1:PkJoWUt/zacQKysNfQtcw1RW+eK2SxkieVBtl+4ovLA= +github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= +github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8= +github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be h1:yzmWtPyxEUIKdZg4RcPq64MfS8NA6A5fNOJgYhpR9EQ= +github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..318234e --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "log" + + "github.com/gchaincl/mempool/client" + "github.com/gchaincl/mempool/ui" +) + +func main() { + c, err := client.New() + if err != nil { + log.Fatal(err) + } + + gui, err := ui.New() + if err != nil { + log.Fatal(err) + } + defer gui.Close() + + go func() { + 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/ui.go b/ui/ui.go new file mode 100644 index 0000000..9460f95 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,228 @@ +package ui + +import ( + "bytes" + "fmt" + "math" + "strings" + "time" + + "github.com/fatih/color" + "github.com/gchaincl/mempool/client" + "github.com/jroimartin/gocui" +) + +const ( + BLOCK_WIDTH = 22 + BLOCKS_TO_DISPLAY = 4 +) + +type UI struct { + gui *gocui.Gui +} + +func New() (*UI, error) { + gui, err := gocui.NewGui(gocui.Output256) + if err != nil { + return nil, err + } + + gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit) + + return &UI{gui: gui}, 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.gui.Update(func(g *gocui.Gui) error { + return ui.update(g, resp) + }) +} + +func (ui *UI) update(g *gocui.Gui, resp *client.Response) error { + x, y := g.Size() + + // whether or not use vertical layout + vertical := BLOCK_WIDTH*5 > x + + // draw projected blocks (mempool) + for i, _ := range resp.ProjectedBlocks { + name := fmt.Sprintf("projected-block-%d", i) + var x0, x1, y0, y1 int + if vertical { + x0 = x - (x/4)*(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.Clear() + if _, err := v.Write(printProjectedBlock(i, resp)); err != nil { + return err + } + } + + if err := ui.separator(g, x, y, vertical); err != nil { + return err + } + + // Copy the last BLOCKS_TO_DISPLAY blocks to a slice + nBlocks := len(resp.Blocks) + if nBlocks > BLOCKS_TO_DISPLAY { + nBlocks = BLOCKS_TO_DISPLAY + } + blocks := make([]*client.Block, nBlocks) + for i := 0; i < nBlocks; i++ { + blocks[i] = &resp.Blocks[len(resp.Blocks)-1-i] + } + + // draw blockchain blocks + for i, block := range blocks { + name := fmt.Sprintf("block-%d", i) + var x0, x1, y0, y1 int + if vertical { + x0 = x - (x/4)*(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.Title = fmt.Sprintf("#%d", block.Height) + v.Clear() + if _, err := v.Write(printBlock(i, blocks)); err != nil { + return err + } + } + + return nil +} + +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("separtor", 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 +} + +var ( + white = color.New(color.FgWhite).SprintfFunc() + yellow = color.New(color.FgYellow).SprintfFunc() +) + +func printProjectedBlock(n int, resp *client.Response) []byte { + block := resp.ProjectedBlocks[n] + + lines := []string{ + white(" ~%.3f sat/vB ", block.MedianFee), + yellow(" %.0f - %.0f sat/vB ", math.Ceil(block.MinFee), math.Ceil(block.MaxFee)), + " ", + white(" %.2f MB ", float64(block.BlockSize)/(1000*1000)), + white(" %4d transactions ", block.NTx), + " ", + " ", + " ", + " ", + " ", + } + + if n < 3 { + lines[8] = white(" in ~%2d minutes ", (n+1)*10) + bg := color.New(color.BgRed).SprintfFunc() + offset := 9 - int( + float64(block.BlockWeight)/4000000.0*10, + ) + for i := offset; i < len(lines); i++ { + lines[i] = bg("%s", lines[i]) + } + } else { + bw := math.Ceil(float64(block.BlockWeight) / 4000000.0) + lines[8] = white(" +%d blocks", int(bw)) + } + + buf := bytes.NewBuffer(nil) + fmt.Fprintf(buf, strings.Join(lines, "\n")) + return buf.Bytes() +} + +func printBlock(n int, blocks []*client.Block) []byte { + block := blocks[n] + + ago := time.Now().Unix() - int64(block.Time) + lines := []string{ + white(" ~%.3f sat/Vb ", block.MedianFee), + yellow(" %.0f - %.0f sat/vB ", math.Ceil(block.MinFee), math.Ceil(block.MaxFee)), + " ", + white(" %.2f MB ", float64(block.Size)/(1000*1000)), + white(" %4d transactions ", block.NTx), + " ", + " ", + " ", + white(" %d secs ago ", ago), + " ", + } + + bg := color.New(color.BgBlue).SprintFunc() + for i, l := range lines { + lines[i] = bg(l) + } + + buf := bytes.NewBuffer(nil) + fmt.Fprintf(buf, strings.Join(lines, "\n")) + return buf.Bytes() +}