Initial fee distribution implementation

This commit is contained in:
Gustavo Chain 2020-02-21 20:13:49 +01:00
parent 6d6631fc62
commit ef1b2e12ba
No known key found for this signature in database
GPG Key ID: DA7C1746DC118A46
3 changed files with 222 additions and 5 deletions

View File

@ -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
}

143
ui/fee_distribution.go Normal file
View File

@ -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
}

View File

@ -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
}