Compare commits

...

47 Commits

Author SHA1 Message Date
Gustavo Chain
fdb8d4002f
update broken ws endpoint
Some checks failed
release / goreleaser (push) Has been cancelled
2023-03-27 11:34:27 +02:00
Gustavo Chaín
1e149e7a50
Merge pull request #15 from mempool/dependabot/go_modules/github.com/fatih/color-1.14.1
Bump github.com/fatih/color from 1.13.0 to 1.14.1
2023-01-30 16:37:02 +00:00
dependabot[bot]
c85f24abe7
Bump github.com/fatih/color from 1.13.0 to 1.14.1
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.13.0 to 1.14.1.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.13.0...v1.14.1)

---
updated-dependencies:
- dependency-name: github.com/fatih/color
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-24 15:07:00 +00:00
naveensrinivasan
d6320efe88
Updated the release to use go 1.17 2022-05-05 20:13:16 +02:00
naveensrinivasan
7461e85495
Upgraded to go 1.17
- Upgraded to go 1.17 as 1.14 is not supported anymore.
- Pinned the docker file to a hash https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies

Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>
2022-05-05 20:13:13 +02:00
Gustavo Chain
595040d46f
go get -u 2022-05-05 20:12:46 +02:00
Naveen
9b11358fb2
Create dependabot.yml 2022-03-06 10:19:24 -05:00
Gustavo Chain
dc9662b509
Update client's endpoint
Some checks failed
release / goreleaser (push) Has been cancelled
2021-01-22 13:21:33 +01:00
Gustavo Chain
1ac079b082
Add halving banner
Some checks failed
release / goreleaser (push) Has been cancelled
2020-04-10 19:20:14 +02:00
Gustavo Chain
ba84b32f05
rename fee_range to feeRange 2020-04-07 10:21:33 +02:00
Gustavo Chain
5e57b325d9
Add Dockerfile
Some checks failed
release / goreleaser (push) Has been cancelled
This commit also disable the docker building process performed by
 goreleaser. Docker containers are built now by hub.docker.com.
2020-03-31 20:42:03 +02:00
Gustavo Chain
89085c721c
Update Dockefile
Some checks failed
release / goreleaser (push) Has been cancelled
2020-03-29 23:04:56 +02:00
Gustavo Chain
e6b74bc6da
Update README.md and goreleaser 2020-03-29 22:58:52 +02:00
Gustavo Chain
07a4573c46
Rename to mempool/mempool-cli 2020-03-29 22:57:15 +02:00
Gustavo Chain
7d6d22186f
Update README.md 2020-03-29 16:32:14 +02:00
Gustavo Chain
395198c4e7
rename projected to mempool 2020-03-29 16:30:31 +02:00
Gustavo Chain
89f05b151b
drop freebsd releases
Some checks failed
release / goreleaser (push) Has been cancelled
2020-03-29 16:15:02 +02:00
Gustavo Chain
252edaf84b
Update code to use new v2 API 2020-03-29 16:05:20 +02:00
Gustavo Chain
616593ce4d
Rename projected to mempool 2020-03-29 15:50:20 +02:00
Gustavo Chaín
f5cdc20b4f
Update README.md 2020-02-27 23:16:57 +01:00
Gustavo Chain
fd071a4aff
Update README.md 2020-02-27 19:43:56 +01:00
Gustavo Chain
37a40a30f9
Fix docker in actions 2020-02-27 19:39:53 +01:00
Gustavo Chaín
71d5c58193
Update README.md 2020-02-27 19:32:47 +01:00
Gustavo Chain
187d5aa88a
Expand releases
Freebsd and Windows added to the OSs
arm and arm64 added to the Archs
Added Docker
2020-02-27 18:47:30 +01:00
Gustavo Chain
1ec586f409
Add tx tracking
Some checks failed
release / goreleaser (push) Has been cancelled
2020-02-27 17:46:55 +01:00
Gustavo Chain
98f1ccbfdc
Update README.md 2020-02-22 09:25:12 +01:00
Gustavo Chain
e5642940f2
Code reorg and feedist coloring
Some checks failed
release / goreleaser (push) Has been cancelled
2020-02-22 09:20:35 +01:00
Gustavo Chain
c28fc0167a
Fee distribution refactor 2020-02-22 08:57:17 +01:00
Gustavo Chain
ef1b2e12ba
Initial fee distribution implementation 2020-02-21 20:13:49 +01:00
Gustavo Chain
6d6631fc62
skip changelog generation 2020-02-21 13:53:09 +01:00
Gustavo Chain
a8e38344e6
fix typo
Some checks failed
release / goreleaser (push) Has been cancelled
2020-02-21 13:47:39 +01:00
Gustavo Chaín
b6f18d09bc
Add files via upload 2020-02-20 17:49:14 +01:00
Gustavo Chaín
01490dc5bc
Add files via upload 2020-02-20 17:44:49 +01:00
Gustavo Chaín
d5b6957377
Update README.md 2020-02-20 17:41:29 +01:00
Gustavo Chaín
91ebc95eb9
Update README.md 2020-02-18 12:31:31 +01:00
Gustavo Chain
36b4c8bfd3
add vBytes/s label on info bar 2020-02-17 09:30:30 +01:00
Gustavo Chain
26a89e1368
remove .goreleaser hooks
Some checks failed
release / goreleaser (push) Has been cancelled
2020-02-16 11:52:14 +01:00
Gustavo Chain
112a2c5ea0
show vBytes/sec 2020-02-16 11:40:42 +01:00
Gustavo Chain
2c82cf9819
go tidy 2020-02-16 11:37:27 +01:00
Gustavo Chaín
8b45fa49f2
Create release.yml 2020-02-16 10:37:37 +01:00
Gustavo Chain
7fa88f2394
add .goreleaser 2020-02-16 10:29:08 +01:00
Gustavo Chain
b08d455969
fix block filling 2020-02-15 21:27:38 +01:00
Gustavo Chain
6c91f757aa
fix remaining blocks info msg 2020-02-14 23:00:58 +01:00
Gustavo Chain
fd4272886b
add loader 2020-02-14 14:05:21 +01:00
Gustavo Chain
9abe9905d7
ui refactor 2020-02-14 13:55:08 +01:00
Gustavo Chain
2eb974ca2a
fix panic on projecteblocks 2020-02-14 11:54:30 +01:00
Gustavo Chain
25daa49e22
Better layout identification 2020-02-14 10:45:15 +01:00
15 changed files with 784 additions and 260 deletions

11
.github/dependabot.yml vendored Normal file
View 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
View 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
View 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
View 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"]

View File

@ -1,15 +1,36 @@
# mempool
# mempool-cli
[mempool.space](https://mempool.space/) for the terminal
![mempool screenshot](https://github.com/gchaincl/mempool/raw/master/share/screenshot.png)
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

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/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
View File

@ -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
View File

@ -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
View File

@ -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
View 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
View 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
View 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
View File

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