From ef1b2e12ba33c0451518323cfe7c1070512cd404 Mon Sep 17 00:00:00 2001 From: Gustavo Chain Date: Fri, 21 Feb 2020 20:13:49 +0100 Subject: [PATCH] Initial fee distribution implementation --- client/client.go | 58 ++++++++++++++++- ui/fee_distribution.go | 143 +++++++++++++++++++++++++++++++++++++++++ ui/ui.go | 26 ++++++-- 3 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 ui/fee_distribution.go diff --git a/client/client.go b/client/client.go index 8157612..6d22907 100644 --- a/client/client.go +++ b/client/client.go @@ -1,6 +1,17 @@ package client -import "github.com/gorilla/websocket" +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/websocket" +) + +const ( + API_URL = "https://mempool.space/api/v1/" +) type MempoolInfo struct { Size int `json:"size"` @@ -75,3 +86,48 @@ func (c *Client) Read() (*Response, error) { } return &resp, nil } + +type Fees []struct { + FPV float64 `json:"fpv"` +} + +func (f Fees) Len() int { return len(f) } +func (f Fees) Less(i, j int) bool { return f[i].FPV < f[j].FPV } +func (f Fees) Swap(i, j int) { f[i], f[j] = f[j], f[i] } + +func Get(ctx context.Context, path string, v interface{}) error { + req, err := http.NewRequest("GET", API_URL+path, nil) + if err != nil { + return err + } + req = req.WithContext(ctx) + + r, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + defer r.Body.Close() + + if s := r.StatusCode; s != 200 { + return fmt.Errorf("status %d", s) + } + + return json.NewDecoder(r.Body).Decode(v) +} + +func GetProjectedFee(ctx context.Context, n int) (Fees, error) { + var fees Fees + if err := Get(ctx, fmt.Sprintf("transactions/projected/%d", n), &fees); err != nil { + return nil, err + } + return fees, nil +} + +func GetBlockFee(ctx context.Context, n int) (Fees, error) { + var fees Fees + if err := Get(ctx, fmt.Sprintf("transactions/height/%d", n), &fees); err != nil { + return nil, err + } + return fees, nil +} diff --git a/ui/fee_distribution.go b/ui/fee_distribution.go new file mode 100644 index 0000000..723b8b0 --- /dev/null +++ b/ui/fee_distribution.go @@ -0,0 +1,143 @@ +package ui + +import ( + "context" + "fmt" + "sort" + "sync" + + "github.com/gchaincl/mempool/client" + "github.com/jroimartin/gocui" +) + +type FeeDistribution struct { + gui *gocui.Gui + + m sync.Mutex + loading bool + isProjected bool + cancelFn context.CancelFunc + fees client.Fees +} + +func NewFeeDistribution(g *gocui.Gui) *FeeDistribution { + return &FeeDistribution{gui: g} +} + +func (fd *FeeDistribution) newCtx() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + fd.cancelFn = cancel + return ctx +} + +func (fd *FeeDistribution) FetchProjection(n int) error { + fd.m.Lock() + defer fd.m.Unlock() + + if fn := fd.cancelFn; fn != nil { + fn() + } + fd.loading = true + + ctx := fd.newCtx() + go func() { + fees, err := client.GetProjectedFee(ctx, n) + if err != nil { + return + } + + fd.fees = fees + fd.loading = false + fd.gui.Update(fd.Layout) + }() + + return nil +} + +func (fd *FeeDistribution) FetchBlock(n int) error { + fd.m.Lock() + defer fd.m.Unlock() + + if fn := fd.cancelFn; fn != nil { + fn() + } + fd.loading = true + + ctx := fd.newCtx() + go func() { + fees, err := client.GetBlockFee(ctx, n) + if err != nil { + return + } + + fd.fees = fees + fd.loading = false + fd.gui.Update(fd.Layout) + }() + + return nil +} + +func (fd *FeeDistribution) Layout(g *gocui.Gui) error { + fd.m.Lock() + defer fd.m.Unlock() + + if fd.loading == false && (fd.fees == nil || len(fd.fees) == 0) { + return nil + } + + x, y := g.Size() + name := "fee_distribution" + v, err := g.SetView(name, x/2-20, y/2-7, x/2+20, y/2+7) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + + v.Title = "Fee distribution" + g.SetCurrentView(name) + g.SetViewOnTop(name) + g.SetKeybinding(name, gocui.KeyEsc, gocui.ModNone, fd.close) + } + + v.Clear() + + if fd.loading == true { + fmt.Fprint(v, "Loading...") + return nil + } + + min, max := 99999, 0 + for _, f := range fd.fees { + fee := int(f.FPV) + if fee < min { + min = fee + } + if fee > max { + max = fee + } + } + fmt.Fprintf(v, "Fee span: %d - %d sat/vByte\n", min, max) + + fmt.Fprintf(v, "Tx count: %d transactions\n", len(fd.fees)) + + sort.Sort(fd.fees) + fmt.Fprintf(v, "Median: ~%d sat/vBytes", int(fd.fees[len(fd.fees)/2].FPV)) + + return nil +} + +func (fd *FeeDistribution) close(g *gocui.Gui, v *gocui.View) error { + fd.m.Lock() + defer fd.m.Unlock() + + if fn := fd.cancelFn; fn != nil { + fn() + } + + fd.cancelFn = nil + fd.loading = false + fd.fees = nil + g.DeleteView(v.Name()) + return nil +} diff --git a/ui/ui.go b/ui/ui.go index 87bcf6b..2176c5e 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "math" + "strconv" "strings" "github.com/fatih/color" @@ -24,6 +25,7 @@ type state struct { type UI struct { gui *gocui.Gui + fd *FeeDistribution state state } @@ -34,12 +36,13 @@ func New() (*UI, error) { } ui := &UI{gui: gui} - gui.SetManager(ui) + ui.fd = NewFeeDistribution(gui) + gui.SetManager(ui, ui.fd) gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit) gui.SetKeybinding("", 'q', gocui.ModNone, quit) - gui.SetKeybinding("", gocui.MouseLeft, gocui.ModNone, ui.click) gui.Mouse = true gui.Highlight = true + gui.InputEsc = true gui.SelFgColor = gocui.ColorWhite return ui, nil @@ -128,6 +131,7 @@ func (ui *UI) Layout(g *gocui.Gui) error { return err } v.BgColor = gocui.ColorBlack + g.SetKeybinding(v.Name(), gocui.MouseLeft, gocui.ModNone, ui.onBlockClick) } v.Clear() @@ -142,7 +146,7 @@ func (ui *UI) Layout(g *gocui.Gui) error { // draw blockchain blocks for i, block := range ui.state.blocks { - name := fmt.Sprintf("block-%d", i) + name := fmt.Sprintf("block-%d", block.Height) var x0, x1, y0, y1 int if vertical { x0 = x - (BLOCK_WIDTH+2)*(i+1) @@ -162,6 +166,7 @@ func (ui *UI) Layout(g *gocui.Gui) error { return err } v.BgColor = gocui.ColorBlack + g.SetKeybinding(v.Name(), gocui.MouseLeft, gocui.ModNone, ui.onBlockClick) } v.Title = fmt.Sprintf("#%d", block.Height) @@ -296,6 +301,19 @@ func fmtSize(s int) string { return fmt.Sprintf("%dMB", ceil(m)) } -func (ui *UI) click(g *gocui.Gui, v *gocui.View) error { +func (ui *UI) onBlockClick(g *gocui.Gui, v *gocui.View) error { + name := v.Name() + if strings.HasPrefix(name, "projected-block-") { + id := strings.TrimPrefix(name, "projected-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 }