listen to mempool.space websocket
This commit is contained in:
@@ -0,0 +1 @@
|
||||
go-rpi-rgb-led-matrix/lib/rpi-rgb-led-matrix/
|
||||
+90
-17
@@ -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:
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tdone:
|
||||
return
|
||||
case <-ticker.C:
|
||||
updateSlots(d, 44000, 821000, 100, 200, 300)
|
||||
d.Clear()
|
||||
d.Render()
|
||||
err = updateSlots(d, price, blockHeight, fees.HourFee, fees.HalfHourFee, fees.FastestFee)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(tdone, ticker)
|
||||
}
|
||||
|
||||
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})
|
||||
|
||||
+25
-27
@@ -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{
|
||||
func New(canvas *rgbmatrix.Canvas, topleft, topright, main, bottom *Slot) *Display {
|
||||
return &Display{
|
||||
canvas: canvas,
|
||||
tl: tl,
|
||||
tr: tr,
|
||||
c: c,
|
||||
bl: bl,
|
||||
br: br,
|
||||
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
@@ -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},
|
||||
|
||||
+99
-9
@@ -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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user