package mempool import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/gorilla/websocket" ) var ( ErrNotWebsocket = errors.New("provided addr was not websocket") ) const ( WSEndpoint = "api/v1/ws" ) type mempool struct { baseUrl string httpClient *http.Client ws *websocket.Conn } func New(baseUrl string, httpClient *http.Client) *mempool { return &mempool{ baseUrl: baseUrl, httpClient: &http.Client{ Timeout: 5 * time.Second, }, } } func (m *mempool) Init() error { u, err := url.Parse(fmt.Sprintf("wss://%s/%s", m.baseUrl, WSEndpoint)) if err != nil { return err } if u.Scheme != "wss" && u.Scheme != "ws" { return fmt.Errorf("%w: got %s", ErrNotWebsocket, u.Scheme) } c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { 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) 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) CurrentPrice() (int, error) { req, err := http.NewRequest(http.MethodGet, "https://api.coincap.io/v2/assets/bitcoin", 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, err } if resp.StatusCode >= http.StatusBadRequest { return 0, err } b, err := io.ReadAll(resp.Body) if err != nil { return 0, err } var data struct { Data struct { PriceUSD string `json:"priceUsd"` } `json:"data"` } err = json.Unmarshal(b, &data) if err != nil { return 0, err } parts := strings.Split(data.Data.PriceUSD, ".") i, err := strconv.Atoi(parts[0]) return i, err } func (m *mempool) Listen(ctx context.Context, messages chan Message, errs chan error, done chan struct{}) { for { select { case <-ctx.Done(): close(messages) close(errs) close(done) return default: _, message, err := m.ws.ReadMessage() if err != nil { errs <- err continue } var m Message err = json.Unmarshal(message, &m) if err != nil { errs <- err continue } messages <- m } } } func (m *mempool) Shutdown(ctx context.Context) error { err := m.ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { return err } m.ws.Close() return nil }