Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb8d4002f | ||
|
|
1e149e7a50 | ||
|
|
c85f24abe7 | ||
|
|
d6320efe88 | ||
|
|
7461e85495 | ||
|
|
595040d46f | ||
|
|
9b11358fb2 | ||
|
|
dc9662b509 | ||
|
|
1ac079b082 | ||
|
|
ba84b32f05 | ||
|
|
5e57b325d9 | ||
|
|
89085c721c | ||
|
|
e6b74bc6da | ||
|
|
07a4573c46 | ||
|
|
7d6d22186f | ||
|
|
395198c4e7 | ||
|
|
89f05b151b | ||
|
|
252edaf84b | ||
|
|
616593ce4d | ||
|
|
f5cdc20b4f | ||
|
|
fd071a4aff | ||
|
|
37a40a30f9 | ||
|
|
71d5c58193 | ||
|
|
187d5aa88a | ||
|
|
1ec586f409 | ||
|
|
98f1ccbfdc | ||
|
|
e5642940f2 | ||
|
|
c28fc0167a | ||
|
|
ef1b2e12ba | ||
|
|
6d6631fc62 | ||
|
|
a8e38344e6 | ||
|
|
b6f18d09bc | ||
|
|
01490dc5bc | ||
|
|
d5b6957377 | ||
|
|
91ebc95eb9 | ||
|
|
36b4c8bfd3 | ||
|
|
26a89e1368 | ||
|
|
112a2c5ea0 | ||
|
|
2c82cf9819 | ||
|
|
8b45fa49f2 | ||
|
|
7fa88f2394 | ||
|
|
b08d455969 | ||
|
|
6c91f757aa | ||
|
|
fd4272886b | ||
|
|
9abe9905d7 | ||
|
|
2eb974ca2a | ||
|
|
25daa49e22 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
33
.github/workflows/release.yml
vendored
Normal file
33
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Unshallow
|
||||
run: git fetch --prune --unshallow
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
-
|
||||
name: Docker login
|
||||
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
33
.goreleaser.yml
Normal file
33
.goreleaser.yml
Normal file
@ -0,0 +1,33 @@
|
||||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
builds:
|
||||
-
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
linux: Linux
|
||||
freebsd: FreeBSD
|
||||
darwin: Darwin
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
skip: true
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM golang:1.17-alpine@sha256:b35984144ec2c2dfd6200e112a9b8ecec4a8fd9eff0babaff330f1f82f14cb2a
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mempool-cli .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=0 /src/mempool-cli /
|
||||
ENTRYPOINT ["/mempool-cli"]
|
||||
29
README.md
29
README.md
@ -1,15 +1,36 @@
|
||||
# mempool
|
||||
# mempool-cli
|
||||
|
||||
[mempool.space](https://mempool.space/) for the terminal
|
||||

|
||||
|
||||
Mempool is a mempool visualizer for the Bitcoin blockchain inspired by
|
||||
[mempool.space](https://mempool.space/). It connects to the same API endpoint but the block rendering happens on your terminal.
|
||||
|
||||
:warning: This software is being under developement, things might break :warning:
|
||||
# Install
|
||||
Get a pre-built [release](https://github.com/gchaincl/mempool/releases/latest)
|
||||
|
||||
### development version
|
||||
```bash
|
||||
go get -u github.com/gchaincl/mempool
|
||||
go get -u github.com/mempool/mempool-cli
|
||||
```
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
docker run -it mempool/mempool-cli
|
||||
```
|
||||
|
||||
# Usage
|
||||
### Key bindings
|
||||
Key | Description
|
||||
------------------|--------------------------------------
|
||||
<kbd>Ctrl+c</kbd> | Quit
|
||||
<kbd>f</kbd> | Opens Tx search
|
||||
<kbd>click</kbd> | Opens info for a selected block
|
||||
|
||||
# TODO
|
||||
- [ ] Transaction Tracking (by Tx ID)
|
||||
- [ ] Block details on click
|
||||
- [x] Transaction Tracking (by Tx ID) (using 'f' key)
|
||||
- [x] Block details on click
|
||||
- [ ] Graphs
|
||||
- [ ] Tx weight per second progress bar
|
||||
- [ ] Custom API endpoint
|
||||
|
||||
133
client/client.go
133
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/ws"
|
||||
)
|
||||
|
||||
type MempoolInfo struct {
|
||||
Size int `json:"size"`
|
||||
@ -8,29 +19,36 @@ type MempoolInfo struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
ID string `json:"id"`
|
||||
Height int `json:"height"`
|
||||
TxCount int `json:"tx_count"`
|
||||
Size int `json:"size"`
|
||||
Time int `json:"timestamp"`
|
||||
Weight int `json:"weight"`
|
||||
FeeRange []float64 `json:"feeRange"`
|
||||
MedianFee float64 `json:"medianFee"`
|
||||
}
|
||||
|
||||
type ProjectedBlock 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"`
|
||||
type MempoolBlock struct {
|
||||
BlockSize int `json:"blockSize"`
|
||||
BlockWeight float64 `json:"blockVSize"`
|
||||
NTx int `json:"nTx"`
|
||||
MinWeigthFee float64 `json:"minWeigthFee"`
|
||||
MaxWeigthFee float64 `json:"maxWeigthFee"`
|
||||
MedianFee float64 `json:"medianFee"`
|
||||
FeeRange []float64 `json:"feeRange"`
|
||||
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 {
|
||||
@ -39,7 +57,8 @@ type Response struct {
|
||||
Block *Block `json:"block"`
|
||||
Blocks []Block `json:"blocks"`
|
||||
|
||||
ProjectedBlocks []ProjectedBlock `json:"projectedBlocks"`
|
||||
MempoolBlocks []MempoolBlock `json:"mempool-blocks"`
|
||||
TrackTx TrackTx `json:"track-tx"`
|
||||
|
||||
TxPerSecond float64 `json:"txPerSecond"`
|
||||
VBytesPerSecond int `json:"vBytesPerSecond"`
|
||||
@ -55,16 +74,15 @@ type Client struct {
|
||||
|
||||
func New() (*Client, error) {
|
||||
dialer := websocket.Dialer{}
|
||||
conn, _, err := dialer.Dial("wss://mempool.space/ws", nil)
|
||||
conn, _, err := dialer.Dial("wss://mempool.space/api/v1/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
|
||||
}
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(
|
||||
`{"action": "init"}`,
|
||||
))
|
||||
|
||||
return &Client{conn: conn}, nil
|
||||
}
|
||||
|
||||
@ -75,3 +93,60 @@ 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","mempool-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"`
|
||||
}
|
||||
|
||||
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 GetMempoolFee(ctx context.Context, n int) (Fees, error) {
|
||||
var fees Fees
|
||||
if err := Get(ctx, fmt.Sprintf("transactions/mempool/%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
|
||||
}
|
||||
|
||||
27
go.mod
27
go.mod
@ -1,17 +1,18 @@
|
||||
module github.com/gchaincl/mempool
|
||||
module github.com/mempool/mempool-cli
|
||||
|
||||
go 1.13
|
||||
go 1.17
|
||||
|
||||
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
|
||||
github.com/fatih/color v1.14.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jroimartin/gocui v0.5.0
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/nsf/termbox-go v1.1.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
)
|
||||
|
||||
95
go.sum
95
go.sum
@ -1,74 +1,21 @@
|
||||
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=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
|
||||
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
|
||||
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
19
main.go
19
main.go
@ -3,33 +3,16 @@ package main
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gchaincl/mempool/client"
|
||||
"github.com/gchaincl/mempool/ui"
|
||||
"github.com/mempool/mempool-cli/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)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 179 KiB |
114
ui/blocks.go
Normal file
114
ui/blocks.go
Normal file
@ -0,0 +1,114 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/mempool/mempool-cli/client"
|
||||
)
|
||||
|
||||
type Box struct {
|
||||
x int
|
||||
lines []string
|
||||
}
|
||||
|
||||
func (b *Box) Printf(c color.Attribute, f string, args ...interface{}) *Box {
|
||||
s := b.center(fmt.Sprintf(f, args...))
|
||||
s = color.New(c).Sprint(s)
|
||||
b.lines = append(b.lines, s)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Box) Append(s string) *Box {
|
||||
b.lines = append(b.lines, b.center(s))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Box) center(line string) string {
|
||||
l := len(line)
|
||||
if l > b.x {
|
||||
return line
|
||||
}
|
||||
offset := strings.Repeat(" ", (b.x-l)/2)
|
||||
return fmt.Sprintf("%s%s%s", offset, line, offset)
|
||||
}
|
||||
|
||||
func (b *Box) Render(full int, bg color.Attribute) []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
fn := color.New(bg).SprintfFunc()
|
||||
for i, s := range b.lines {
|
||||
if 9-full <= i {
|
||||
s = fn("%s", s)
|
||||
}
|
||||
fmt.Fprintln(buf, s)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
type MempoolBlock client.MempoolBlock
|
||||
|
||||
func (b MempoolBlock) Print(n int, x, _y int) []byte {
|
||||
var footer string
|
||||
// Attach ETA to the first 3 blocks
|
||||
if n < 3 {
|
||||
footer = fmt.Sprintf("in ~%d minutes", (n+1)*10)
|
||||
} else {
|
||||
n := ceil(float64(b.BlockWeight) / 4000000.0)
|
||||
footer = fmt.Sprintf("+%d blocks", n)
|
||||
}
|
||||
|
||||
var min, max float64
|
||||
if len(b.FeeRange) > 2 {
|
||||
min, max = b.FeeRange[0], b.FeeRange[len(b.FeeRange)-1]
|
||||
}
|
||||
box := &Box{x: x}
|
||||
box.Printf(color.FgWhite, "~%d sat/vB", ceil(b.MedianFee)).
|
||||
Printf(color.FgYellow, "%d-%d sat/vB", ceil(min), ceil(max)).
|
||||
Append("").
|
||||
Printf(color.FgWhite, "%.2f MB", float64(b.BlockSize)/(1000*1000)).
|
||||
Printf(color.FgWhite, "%4d transactions", b.NTx).
|
||||
Append("").
|
||||
Append("").
|
||||
Append("").
|
||||
Printf(color.FgWhite, footer)
|
||||
|
||||
// calculate how full is the block
|
||||
var full int
|
||||
if n < 3 {
|
||||
full = int(
|
||||
float64(b.BlockWeight) / 1_000_000 * 10,
|
||||
)
|
||||
}
|
||||
|
||||
return box.Render(full, color.BgRed)
|
||||
}
|
||||
|
||||
type Block client.Block
|
||||
|
||||
func (b Block) Print(n int, x, _y int) []byte {
|
||||
ago := time.Now().Unix() - int64(b.Time)
|
||||
box := &Box{x: x}
|
||||
|
||||
var min, max float64
|
||||
if len(b.FeeRange) > 2 {
|
||||
min, max = b.FeeRange[0], b.FeeRange[len(b.FeeRange)-1]
|
||||
}
|
||||
box.Printf(color.FgWhite, "~%d sat/Vb", ceil(b.MedianFee)).
|
||||
Printf(color.FgYellow, "%d-%d sat/vB", ceil(min), ceil(max)).
|
||||
Append("").
|
||||
Printf(color.FgWhite, "%.2f MB", float64(b.Size)/(1000*1000)).
|
||||
Printf(color.FgWhite, " %4d transactions", b.TxCount).
|
||||
Append("").
|
||||
Append("").
|
||||
Append("").
|
||||
Printf(color.FgWhite, "%s ago", fmtSeconds(ago))
|
||||
|
||||
full := int(
|
||||
float64(b.Weight) / 4_000_000 * 10,
|
||||
)
|
||||
|
||||
return box.Render(full, color.BgBlue)
|
||||
}
|
||||
131
ui/fee_distribution.go
Normal file
131
ui/fee_distribution.go
Normal file
@ -0,0 +1,131 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jroimartin/gocui"
|
||||
|
||||
"github.com/mempool/mempool-cli/client"
|
||||
)
|
||||
|
||||
type FeeDistribution struct {
|
||||
gui *gocui.Gui
|
||||
|
||||
m sync.Mutex
|
||||
loading bool
|
||||
isProjected bool
|
||||
cancelFn context.CancelFunc
|
||||
fees client.Fees
|
||||
title string
|
||||
}
|
||||
|
||||
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 {
|
||||
fn := func(ctx context.Context) (client.Fees, error) {
|
||||
return client.GetMempoolFee(ctx, n)
|
||||
}
|
||||
return fd.fetch(fn)
|
||||
}
|
||||
|
||||
func (fd *FeeDistribution) FetchBlock(n int) error {
|
||||
fn := func(ctx context.Context) (client.Fees, error) {
|
||||
return client.GetBlockFee(ctx, n)
|
||||
}
|
||||
return fd.fetch(fn)
|
||||
}
|
||||
|
||||
func (fd *FeeDistribution) fetch(fn func(ctx context.Context) (client.Fees, error)) 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 := fn(ctx)
|
||||
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-3, x/2+20, y/2+3)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
v.Title = "Fee distribution" + fd.title + "('esc' to close)"
|
||||
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
|
||||
}
|
||||
|
||||
sort.Sort(fd.fees)
|
||||
txs := len(fd.fees)
|
||||
min, max := fd.fees[0].FPV, fd.fees[txs-1].FPV
|
||||
|
||||
var (
|
||||
white = color.New(color.FgWhite).SprintfFunc()
|
||||
yellow = color.New(color.FgYellow).SprintfFunc()
|
||||
)
|
||||
fmt.Fprintf(v, white("Fee span:")+" %d - %d "+yellow("sat/vByte\n"), ceil(min), ceil(max))
|
||||
fmt.Fprintf(v, white("Tx count:")+" %d "+white("transactions\n"), txs)
|
||||
fmt.Fprintf(v, white("Median: ")+" ~%d "+yellow("sat/vBytes\n"), ceil(fd.fees[txs/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
|
||||
}
|
||||
88
ui/tx_search.go
Normal file
88
ui/tx_search.go
Normal file
@ -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
|
||||
}
|
||||
289
ui/ui.go
289
ui/ui.go
@ -1,30 +1,37 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/gchaincl/mempool/client"
|
||||
"github.com/jroimartin/gocui"
|
||||
|
||||
"github.com/mempool/mempool-cli/client"
|
||||
)
|
||||
|
||||
const (
|
||||
BLOCK_WIDTH = 22
|
||||
BLOCK_WIDTH = 22
|
||||
NEXT_HALVING = 630_000
|
||||
)
|
||||
|
||||
type state struct {
|
||||
blocks []client.Block
|
||||
projected []client.ProjectedBlock
|
||||
info *client.MempoolInfo
|
||||
loaded bool
|
||||
blocks []client.Block
|
||||
mempool []client.MempoolBlock
|
||||
vBytesPerSecond int
|
||||
info *client.MempoolInfo
|
||||
tracking *client.TrackTx
|
||||
}
|
||||
|
||||
type UI struct {
|
||||
gui *gocui.Gui
|
||||
state state
|
||||
client *client.Client
|
||||
gui *gocui.Gui
|
||||
fd *FeeDistribution
|
||||
ts *TXSearch
|
||||
state state
|
||||
}
|
||||
|
||||
func New() (*UI, error) {
|
||||
@ -34,14 +41,41 @@ func New() (*UI, error) {
|
||||
}
|
||||
|
||||
ui := &UI{gui: gui}
|
||||
gui.SetManager(ui)
|
||||
ui.fd = NewFeeDistribution(gui)
|
||||
ui.ts = NewTXSearch(gui)
|
||||
gui.SetManager(ui, ui.fd, ui.ts)
|
||||
|
||||
gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit)
|
||||
gui.SetKeybinding("", 'q', gocui.ModNone, quit)
|
||||
gui.SetKeybinding("", gocui.MouseLeft, gocui.ModNone, ui.click)
|
||||
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
|
||||
}
|
||||
|
||||
@ -59,6 +93,10 @@ func (ui *UI) Loop() error {
|
||||
}
|
||||
|
||||
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++ {
|
||||
@ -69,8 +107,8 @@ func (ui *UI) Render(resp *client.Response) {
|
||||
ui.state.blocks = bs
|
||||
}
|
||||
|
||||
if bs := resp.ProjectedBlocks; len(bs) != 0 {
|
||||
ui.state.projected = bs
|
||||
if bs := resp.MempoolBlocks; len(bs) != 0 {
|
||||
ui.state.mempool = bs
|
||||
}
|
||||
|
||||
if b := resp.Block; b != nil {
|
||||
@ -81,6 +119,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())
|
||||
@ -92,14 +133,21 @@ func (ui *UI) Render(resp *client.Response) {
|
||||
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*8 > x
|
||||
vertical := BLOCK_WIDTH*6 > x
|
||||
|
||||
// draw projected blocks (mempool)
|
||||
for i, _ := range ui.state.projected {
|
||||
name := fmt.Sprintf("projected-block-%d", i)
|
||||
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)
|
||||
@ -119,9 +167,19 @@ func (ui *UI) Layout(g *gocui.Gui) error {
|
||||
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.printProjectedBlock(i)); err != nil {
|
||||
if _, err := v.Write(ui.printMempoolBlock(i, x1-x0, y1-y0)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -132,7 +190,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)
|
||||
@ -152,10 +210,20 @@ 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)
|
||||
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)); err != nil {
|
||||
if _, err := v.Write(ui.printBlock(i, x1-x0, y1-y0)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -164,9 +232,36 @@ func (ui *UI) Layout(g *gocui.Gui) error {
|
||||
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 {
|
||||
@ -177,7 +272,7 @@ func (ui *UI) separator(g *gocui.Gui, x, y int, vertical bool) error {
|
||||
y0, y1 = 0, y
|
||||
}
|
||||
|
||||
v, err := g.SetView("separtor", x0, y0, x1, y1)
|
||||
v, err := g.SetView("separator", x0, y0, x1, y1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
@ -196,14 +291,13 @@ func (ui *UI) separator(g *gocui.Gui, x, y int, vertical bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
white = color.New(color.FgWhite).SprintfFunc()
|
||||
yellow = color.New(color.FgYellow).SprintfFunc()
|
||||
red = color.New(color.FgRed).SprintfFunc()
|
||||
blue = color.New(color.FgBlue).SprintfFunc()
|
||||
)
|
||||
|
||||
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 {
|
||||
@ -219,109 +313,60 @@ func (ui *UI) info(g *gocui.Gui, x, y int) error {
|
||||
}
|
||||
|
||||
var mSize int
|
||||
for _, b := range ui.state.projected {
|
||||
for _, b := range ui.state.mempool {
|
||||
mSize += b.BlockSize
|
||||
}
|
||||
|
||||
fmt.Fprintf(v, "%s %s, %s %s",
|
||||
// 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 block/s)", fmtSize(mSize), len(ui.state.projected)),
|
||||
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 ceil(f float64) int {
|
||||
return int(
|
||||
math.Ceil(f),
|
||||
)
|
||||
}
|
||||
|
||||
func (ui *UI) printProjectedBlock(n int) []byte {
|
||||
block := ui.state.projected[n]
|
||||
|
||||
lines := []string{
|
||||
white(" ~%d sat/vB ", ceil(block.MedianFee)),
|
||||
yellow(" %d - %d sat/vB ", ceil(block.MinFee), ceil(block.MaxFee)),
|
||||
" ",
|
||||
white(" %.2f MB ", float64(block.BlockSize)/(1000*1000)),
|
||||
white(" %4d transactions ", block.NTx),
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
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
|
||||
}
|
||||
|
||||
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])
|
||||
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
|
||||
}
|
||||
} 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 (ui *UI) printBlock(n int) []byte {
|
||||
block := ui.state.blocks[n]
|
||||
|
||||
ago := time.Now().Unix() - int64(block.Time)
|
||||
lines := []string{
|
||||
white(" ~%d sat/Vb ", ceil(block.MedianFee)),
|
||||
yellow(" %d - %d sat/vB ", ceil(block.MinFee), ceil(block.MaxFee)),
|
||||
" ",
|
||||
white(" %.2f MB ", float64(block.Size)/(1000*1000)),
|
||||
white(" %4d transactions ", block.NTx),
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
white(" %s ago ", fmtSeconds(ago)),
|
||||
" ",
|
||||
}
|
||||
|
||||
bg := color.New(color.BgBlue).SprintfFunc()
|
||||
offset := 9 - int(
|
||||
float64(block.Weight)/4000000.0*10,
|
||||
)
|
||||
for i := offset; i < len(lines); i++ {
|
||||
lines[i] = bg("%s", lines[i])
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
fmt.Fprintf(buf, strings.Join(lines, "\n"))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func fmtSeconds(s int64) string {
|
||||
if s < 60 {
|
||||
return "< 1 minute"
|
||||
} else if s < 120 {
|
||||
return fmt.Sprintf("1 minute")
|
||||
} else if s < 3600 {
|
||||
return fmt.Sprintf("%d minutes", s/60)
|
||||
}
|
||||
return fmt.Sprintf("%d hours", s/3600)
|
||||
}
|
||||
|
||||
func fmtSize(s int) string {
|
||||
if s < 1024*1024 {
|
||||
m := float64(s) / 1000.0
|
||||
return fmt.Sprintf("%dkB", ceil(m))
|
||||
}
|
||||
m := float64(s) / (1000.0 * 1000.0)
|
||||
return fmt.Sprintf("%dMB", ceil(m))
|
||||
}
|
||||
|
||||
func (ui *UI) click(g *gocui.Gui, v *gocui.View) error {
|
||||
_, err := g.SetCurrentView(v.Name())
|
||||
_, 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
|
||||
}
|
||||
|
||||
32
ui/util.go
Normal file
32
ui/util.go
Normal file
@ -0,0 +1,32 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
func ceil(f float64) int {
|
||||
return int(
|
||||
math.Ceil(f),
|
||||
)
|
||||
}
|
||||
|
||||
func fmtSeconds(s int64) string {
|
||||
if s < 60 {
|
||||
return "< 1 minute"
|
||||
} else if s < 120 {
|
||||
return fmt.Sprintf("1 minute")
|
||||
} else if s < 3600 {
|
||||
return fmt.Sprintf("%d minutes", s/60)
|
||||
}
|
||||
return fmt.Sprintf("%d hours", s/3600)
|
||||
}
|
||||
|
||||
func fmtSize(s int) string {
|
||||
if s < 1024*1024 {
|
||||
m := float64(s) / 1000.0
|
||||
return fmt.Sprintf("%dkB", ceil(m))
|
||||
}
|
||||
m := float64(s) / (1000.0 * 1000.0)
|
||||
return fmt.Sprintf("%dMB", ceil(m))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user