listen to mempool.space websocket

This commit is contained in:
2023-12-21 17:33:20 -05:00
parent 9e83d6cc7d
commit babd5d8253
6 changed files with 254 additions and 68 deletions
+1
View File
@@ -0,0 +1 @@
go-rpi-rgb-led-matrix/lib/rpi-rgb-led-matrix/
+92 -19
View File
@@ -5,6 +5,8 @@ import (
"fmt"
"image/color"
"log"
"net/http"
"sync"
"time"
"git.dvdt.dev/david/bitcoin-ticker-pi/display"
@@ -14,8 +16,8 @@ import (
)
const (
RefreshRateHz = 60
MempoolURL = "wss://mempool.dvdt.dev/v1/api/ws"
RefreshRateHz = 1
MempoolURL = "mempool.space"
)
func main() {
@@ -31,38 +33,92 @@ func main() {
display.NewSlot(5, 5, 4, 44, 0),
display.NewSlot(7, 13, 8, 5, 9),
display.NewSlot(12, 5, 4, 7, 27),
display.NewSlot(8, 5, 4, 28, 27),
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ticker := time.NewTicker(time.Second / RefreshRateHz)
tdone := make(chan struct{})
mdone := make(chan struct{})
blocks := make(chan mempool.Block)
msgs := make(chan mempool.Message)
errs := make(chan error)
mp := mempool.New(MempoolURL)
mp := mempool.New(MempoolURL, &http.Client{
Timeout: 5 * time.Second,
})
err := mp.Init()
if err != nil {
log.Fatal(err)
}
go func(done chan struct{}, t *time.Ticker) {
currentBlockHeight, err := mp.CurrentBlockHeight()
if err != nil {
log.Fatal(err)
}
highfee, medfee, lowfee, err := mp.CurrentFees()
if err != nil {
log.Fatal(err)
}
var blockHeight int
blockHeightMutex := &sync.Mutex{}
var fees mempool.Fees
feesMutex := &sync.Mutex{}
var price int
priceMutex := &sync.Mutex{}
blockHeight = currentBlockHeight
fees.FastestFee = highfee
fees.HalfHourFee = medfee
fees.HourFee = lowfee
go mp.Listen(ctx, msgs, errs, mdone)
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
updateSlots(d, 44000, 821000, 100, 200, 300)
case m := <-msgs:
fmt.Printf("%+v\n", m)
if m.Fees != nil {
feesMutex.Lock()
fees = *m.Fees
feesMutex.Unlock()
}
if m.Block != nil {
blockHeightMutex.Lock()
blockHeight = int(m.Block.Height)
blockHeightMutex.Unlock()
}
if m.Conversions != nil {
priceMutex.Lock()
price = m.Conversions.USD
priceMutex.Unlock()
}
}
}
}(tdone, ticker)
}()
for {
select {
case <-tdone:
return
case <-ticker.C:
d.Clear()
d.Render()
err = updateSlots(d, price, blockHeight, fees.HourFee, fees.HalfHourFee, fees.FastestFee)
if err != nil {
log.Fatal(err)
}
}
}
}
func updateSlots(d *display.Display, price, height, lfee, mfee, hfee int) error {
priceStr := fmt.Sprintf("%d", price)
d.Clear()
var priceSegments []display.Segment
priceSegments = append(priceSegments, strToDisplaySegments("dollar", fonts.Green)...)
priceSegments = append(priceSegments, display.Segment{Text: "dollar", Color: fonts.Green})
priceSegments = append(priceSegments, strToDisplaySegments(fmt.Sprintf("%d", price), fonts.White)...)
err := d.UpdateTopLeft(priceSegments)
@@ -71,8 +127,12 @@ func updateSlots(d *display.Display, price, height, lfee, mfee, hfee int) error
}
var moscowTimeSegments []display.Segment
moscowTimeSegments = append(moscowTimeSegments, strToDisplaySegments("satoshi", fonts.Orange)...)
moscowTimeSegments = append(moscowTimeSegments, strToDisplaySegments(fmt.Sprintf("%d", 100000000/price), fonts.White)...)
moscowTime := 0
if price > 0 {
moscowTime = 100000000 / price
}
moscowTimeSegments = append(moscowTimeSegments, display.Segment{Text: "satoshi", Color: fonts.Orange})
moscowTimeSegments = append(moscowTimeSegments, strToDisplaySegments(fmt.Sprintf("%d", moscowTime), fonts.White)...)
err = d.UpdateTopRight(moscowTimeSegments)
if err != nil {
@@ -80,18 +140,31 @@ func updateSlots(d *display.Display, price, height, lfee, mfee, hfee int) error
}
var heightSegments []display.Segment
heightSegments = append(heightSegments, strToDisplaySegments("height", fonts.Blue)...)
heightSegments = append(heightSegments, display.Segment{Text: "height", Color: fonts.Blue})
heightSegments = append(heightSegments, strToDisplaySegments(fmt.Sprintf("%d", height), fonts.White)...)
err = d.UpdateCenter(heightSegments)
err = d.UpdateMain(heightSegments)
if err != nil {
return err
}
return nil
var feeSegments []display.Segment
feeSegments = append(feeSegments, display.Segment{Text: "low", Color: fonts.Green})
feeSegments = append(feeSegments, strToDisplaySegments(fmt.Sprintf("%d", lfee), fonts.White)...)
feeSegments = append(feeSegments, display.Segment{Text: "medium", Color: fonts.Orange})
feeSegments = append(feeSegments, strToDisplaySegments(fmt.Sprintf("%d", mfee), fonts.White)...)
feeSegments = append(feeSegments, display.Segment{Text: "high", Color: fonts.Red})
feeSegments = append(feeSegments, strToDisplaySegments(fmt.Sprintf("%d", hfee), fonts.White)...)
err = d.UpdateBottom(feeSegments)
if err != nil {
return err
}
return d.Render()
}
func strToDisplaySegments(s string, c color.Color) []display.Segment {
func strToDisplaySegments(s string, c color.RGBA) []display.Segment {
var segs []display.Segment
for _, r := range s {
segs = append(segs, display.Segment{Text: string(r), Color: c})
+26 -28
View File
@@ -1,71 +1,69 @@
package display
import (
"image"
"image/draw"
"git.dvdt.dev/david/bitcoin-ticker-pi/fonts"
rgbmatrix "git.dvdt.dev/david/bitcoin-ticker-pi/go-rpi-rgb-led-matrix"
)
type Display struct {
canvas *rgbmatrix.Canvas
tl, tr, c, bl, br *Slot
topleft, topright, main, bottom *Slot
}
func New(canvas *rgbmatrix.Canvas, tl, tr, c, bl, br *Slot) *Display {
return &display{
canvas: canvas,
tl: tl,
tr: tr,
c: c,
bl: bl,
br: br,
func New(canvas *rgbmatrix.Canvas, topleft, topright, main, bottom *Slot) *Display {
return &Display{
canvas: canvas,
topleft: topleft,
topright: topright,
main: main,
bottom: bottom,
}
}
func (d *Display) UpdateTopLeft(text []Segment) error {
err := d.tl.Update(text)
err := d.topleft.Update(text)
if err != nil {
return err
}
d.tl.Draw(d.canvas)
d.topleft.Draw(d.canvas)
return nil
}
func (d *Display) UpdateTopRight(text []Segment) error {
err := d.tr.Update(text)
err := d.topright.Update(text)
if err != nil {
return err
}
d.tr.Draw(d.canvas)
d.topright.Draw(d.canvas)
return nil
}
func (d *Display) UpdateCenter(text []Segment) error {
err := d.c.Update(text)
func (d *Display) UpdateMain(text []Segment) error {
err := d.main.Update(text)
if err != nil {
return err
}
d.c.Draw(d.canvas)
d.main.Draw(d.canvas)
return nil
}
func (d *Display) UpdateBottomLeft(text []Segment) error {
err := d.bl.Update(text)
func (d *Display) UpdateBottom(text []Segment) error {
err := d.bottom.Update(text)
if err != nil {
return err
}
d.bl.Draw(d.canvas)
return nil
}
func (d *Display) UpdateBottomRight(text []Segment) error {
err := d.br.Update(text)
if err != nil {
return err
}
d.bl.Draw(d.canvas)
d.bottom.Draw(d.canvas)
return nil
}
func (d *Display) Render() error {
return d.canvas.Render()
}
func (d *Display) Clear() {
draw.Draw(d.canvas, d.canvas.Bounds(), &image.Uniform{fonts.Black}, image.ZP, draw.Src)
}
+11 -11
View File
@@ -21,7 +21,7 @@ var (
{0, 1, 1, 1, 1, 1, 0, 0},
{1, 1, 1, 0, 1, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 1, 0, 0, 0},
@@ -108,19 +108,19 @@ var (
{0, 0, 1, 1, 0, 0, 0, 0},
},
"8": {
{0, 0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 0, 0},
{1, 1, 1, 0, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 1, 0, 1, 1, 1, 0},
{0, 1, 1, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 0, 0},
{1, 1, 1, 0, 1, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 1, 0, 1, 1, 1, 0},
{0, 1, 1, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 0, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 1, 1, 1, 1, 0, 0},
},
"9": {
{0, 0, 1, 1, 1, 0, 0, 0},
@@ -134,8 +134,8 @@ var (
{0, 0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 0, 1, 1, 0},
{0, 1, 0, 0, 1, 1, 1, 0},
{0, 1, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 0, 1, 1, 1, 1, 0, 0},
},
"0": {
{0, 1, 1, 1, 1, 1, 0, 0},
+100 -10
View File
@@ -5,7 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/gorilla/websocket"
)
@@ -14,19 +18,27 @@ var (
ErrNotWebsocket = errors.New("provided addr was not websocket")
)
const (
WSEndpoint = "api/v1/ws"
)
type mempool struct {
addr string
ws *websocket.Conn
baseUrl string
httpClient *http.Client
ws *websocket.Conn
}
func New(addr string) *mempool {
func New(baseUrl string, httpClient *http.Client) *mempool {
return &mempool{
addr: addr,
baseUrl: baseUrl,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}
func (m *mempool) Init() error {
u, err := url.Parse(m.addr)
u, err := url.Parse(fmt.Sprintf("wss://%s/%s", m.baseUrl, WSEndpoint))
if err != nil {
return err
}
@@ -40,15 +52,93 @@ func (m *mempool) Init() error {
return err
}
b, err := json.Marshal(struct {
Action string `json:"action"`
Data []string `json:"data"`
}{
Action: "want",
Data: []string{"blocks", "stats"},
})
if err != nil {
return err
}
if err = c.WriteMessage(websocket.BinaryMessage, b); err != nil {
return err
}
m.ws = c
return nil
}
func (m *mempool) Listen(ctx context.Context, blocks chan Block, errs chan error, done chan struct{}) {
func (m *mempool) CurrentBlockHeight() (int, error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/api/blocks/tip/height", m.baseUrl), nil)
if err != nil {
return 0, err
}
resp, err := m.httpClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusInternalServerError {
return 0, errors.New("server error")
}
if resp.StatusCode >= http.StatusBadRequest {
return 0, errors.New("client error")
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
return strconv.Atoi(string(b))
}
func (m *mempool) CurrentFees() (int, int, int, error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/api/v1/fees/recommended", m.baseUrl), nil)
if err != nil {
return 0, 0, 0, err
}
resp, err := m.httpClient.Do(req)
if err != nil {
return 0, 0, 0, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusInternalServerError {
return 0, 0, 0, err
}
if resp.StatusCode >= http.StatusBadRequest {
return 0, 0, 0, err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return 0, 0, 0, err
}
var data struct {
Fastest int `json:"fastestFee"`
HalfHour int `json:"halfHourFee"`
Hour int `json:"hourFee"`
}
err = json.Unmarshal(b, &data)
return data.Fastest, data.HalfHour, data.Hour, err
}
func (m *mempool) Listen(ctx context.Context, messages chan Message, errs chan error, done chan struct{}) {
for {
select {
case <-ctx.Done():
close(blocks)
close(messages)
close(errs)
close(done)
return
@@ -59,14 +149,14 @@ func (m *mempool) Listen(ctx context.Context, blocks chan Block, errs chan error
continue
}
var b Block
err = json.Unmarshal(message, &b)
var m Message
err = json.Unmarshal(message, &m)
if err != nil {
errs <- err
continue
}
blocks <- b
messages <- m
}
}
}
+24
View File
@@ -1,5 +1,29 @@
package mempool
type Message struct {
Block *Block `json:"block"`
Conversions *Conversions `json:"conversions"`
Fees *Fees `json:"fees"`
}
type Block struct {
Height int64 `json:"height"`
}
type Conversions struct {
USD int `json:"USD"`
EUR int `json:"EUR"`
GBP int `json:"GBP"`
CAD int `json:"CAD"`
CHF int `json:"CHF"`
AUD int `json:"AUD"`
JPY int `json:"JPY"`
}
type Fees struct {
FastestFee int `json:"fastestFee"`
HalfHourFee int `json:"halfHourFee"`
HourFee int `json:"hourFee"`
EconomyFee int `json:"economyFee"`
MinimumFee int `json:"minimumFee"`
}