mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
codebase: Remove web frontend and related services (#2067)
* codebase: Remove web frontend and related services * refactor: Update StartPPROF to accept context and adjust related tests * refactor: Simplify SetIfZero functions and update related tests * config: Clarify DowngradeConfig method documentation regarding permanent removal of deprecated fields * refactor: Rename setIfZeroAndWarn to setDefaultIfZeroWarn for clarity and update related calls * refactor: Update error handling in DataHistoryManager and remove redundant error variable
This commit is contained in:
@@ -1,918 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/encoding/json"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// setupAPIServerManager checks and creates an api server manager
|
||||
func setupAPIServerManager(remoteConfig *config.RemoteControlConfig, pprofConfig *config.Profiler, exchangeManager iExchangeManager, bot iBot, portfolioManager iPortfolioManager, configPath string) (*apiServerManager, error) {
|
||||
if remoteConfig == nil {
|
||||
return nil, errNilRemoteConfig
|
||||
}
|
||||
if pprofConfig == nil {
|
||||
return nil, errNilPProfConfig
|
||||
}
|
||||
if exchangeManager == nil {
|
||||
return nil, errNilExchangeManager
|
||||
}
|
||||
if bot == nil {
|
||||
return nil, errNilBot
|
||||
}
|
||||
if configPath == "" {
|
||||
return nil, errEmptyConfigPath
|
||||
}
|
||||
return &apiServerManager{
|
||||
remoteConfig: remoteConfig,
|
||||
pprofConfig: pprofConfig,
|
||||
restListenAddress: remoteConfig.DeprecatedRPC.ListenAddress,
|
||||
websocketListenAddress: remoteConfig.WebsocketRPC.ListenAddress,
|
||||
exchangeManager: exchangeManager,
|
||||
bot: bot,
|
||||
gctConfigPath: configPath,
|
||||
portfolioManager: portfolioManager,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsRESTServerRunning safely checks whether the subsystem is running
|
||||
func (m *apiServerManager) IsRESTServerRunning() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
return atomic.LoadInt32(&m.restStarted) == 1
|
||||
}
|
||||
|
||||
// IsWebsocketServerRunning safely checks whether the subsystem is running
|
||||
func (m *apiServerManager) IsWebsocketServerRunning() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
return atomic.LoadInt32(&m.websocketStarted) == 1
|
||||
}
|
||||
|
||||
// StopRESTServer attempts to shutdown the subsystem
|
||||
func (m *apiServerManager) StopRESTServer() error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("api server %w", ErrNilSubsystem)
|
||||
}
|
||||
if !atomic.CompareAndSwapInt32(&m.restStarted, 1, 0) {
|
||||
return fmt.Errorf("apiserver deprecated server %w", ErrSubSystemNotStarted)
|
||||
}
|
||||
err := m.restHTTPServer.Shutdown(context.Background())
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
m.wgRest.Wait()
|
||||
m.restRouter = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *apiServerManager) StopWebsocketServer() error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("api server %w", ErrNilSubsystem)
|
||||
}
|
||||
if !atomic.CompareAndSwapInt32(&m.websocketStarted, 1, 0) {
|
||||
return fmt.Errorf("apiserver websocket server %w", ErrSubSystemNotStarted)
|
||||
}
|
||||
|
||||
err := m.websocketHTTPServer.Shutdown(context.Background())
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
m.websocketRouter = nil
|
||||
m.websocketHub = nil
|
||||
m.wgWebsocket.Wait()
|
||||
m.websocketHTTPServer = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// newRouter takes in the exchange interfaces and returns a new multiplexer
|
||||
// router
|
||||
func (m *apiServerManager) newRouter(isREST bool) *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
var routes []Route
|
||||
if common.ExtractPortOrDefault(m.websocketListenAddress) == 80 {
|
||||
m.websocketListenAddress = common.ExtractHostOrDefault(m.websocketListenAddress)
|
||||
} else {
|
||||
m.websocketListenAddress = common.ExtractHostOrDefault(m.websocketListenAddress) + ":" +
|
||||
strconv.Itoa(common.ExtractPortOrDefault(m.websocketListenAddress))
|
||||
}
|
||||
|
||||
if isREST {
|
||||
routes = []Route{
|
||||
{"", http.MethodGet, "/", m.getIndex},
|
||||
{"GetAllSettings", http.MethodGet, "/config/all", m.restGetAllSettings},
|
||||
{"SaveAllSettings", http.MethodPost, "/config/all/save", m.restSaveAllSettings},
|
||||
{"AllEnabledAccountInfo", http.MethodGet, "/exchanges/enabled/accounts/all", m.restGetAllEnabledAccountInfo},
|
||||
{"AllActiveExchangesAndCurrencies", http.MethodGet, "/exchanges/enabled/latest/all", m.restGetAllActiveTickers},
|
||||
{"GetPortfolio", http.MethodGet, "/portfolio/all", m.restGetPortfolio},
|
||||
{"AllActiveExchangesAndOrderbooks", http.MethodGet, "/exchanges/orderbook/latest/all", m.restGetAllActiveOrderbooks},
|
||||
}
|
||||
|
||||
if m.pprofConfig.Enabled {
|
||||
if m.pprofConfig.MutexProfileFraction > 0 {
|
||||
runtime.SetMutexProfileFraction(m.pprofConfig.MutexProfileFraction)
|
||||
}
|
||||
log.Debugf(log.RESTSys,
|
||||
"HTTP Go performance profiler (pprof) endpoint enabled: http://%s:%d/debug/pprof/\n",
|
||||
common.ExtractHostOrDefault(m.websocketListenAddress),
|
||||
common.ExtractPortOrDefault(m.websocketListenAddress),
|
||||
)
|
||||
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
|
||||
}
|
||||
} else {
|
||||
routes = []Route{
|
||||
{"ws", http.MethodGet, "/ws", m.WebsocketClientHandler},
|
||||
}
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
router.
|
||||
Methods(route.Method).
|
||||
Path(route.Pattern).
|
||||
Name(route.Name).
|
||||
Handler(restLogger(route.HandlerFunc, route.Name)).
|
||||
Host(m.websocketListenAddress)
|
||||
}
|
||||
return router
|
||||
}
|
||||
|
||||
// StartRESTServer starts a REST handler
|
||||
func (m *apiServerManager) StartRESTServer() error {
|
||||
if !atomic.CompareAndSwapInt32(&m.restStarted, 0, 1) {
|
||||
return fmt.Errorf("rest server %w", errAlreadyRunning)
|
||||
}
|
||||
if !m.remoteConfig.DeprecatedRPC.Enabled {
|
||||
atomic.StoreInt32(&m.restStarted, 0)
|
||||
return fmt.Errorf("rest %w", errServerDisabled)
|
||||
}
|
||||
log.Debugf(log.RESTSys,
|
||||
"Deprecated RPC handler support enabled. Listen URL: http://%s:%d\n",
|
||||
common.ExtractHostOrDefault(m.restListenAddress),
|
||||
common.ExtractPortOrDefault(m.restListenAddress),
|
||||
)
|
||||
m.restRouter = m.newRouter(true)
|
||||
if m.restHTTPServer == nil {
|
||||
m.restHTTPServer = &http.Server{
|
||||
Addr: m.restListenAddress,
|
||||
Handler: m.restRouter,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
}
|
||||
}
|
||||
m.wgRest.Go(func() {
|
||||
err := m.restHTTPServer.ListenAndServe()
|
||||
if err != nil {
|
||||
atomic.StoreInt32(&m.restStarted, 0)
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// restLogger logs the requests internally
|
||||
func restLogger(inner http.Handler, name string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
inner.ServeHTTP(w, r)
|
||||
|
||||
log.Debugf(log.RESTSys,
|
||||
"%s\t%s\t%s\t%s",
|
||||
r.Method,
|
||||
r.RequestURI,
|
||||
name,
|
||||
time.Since(start),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// writeResponse outputs a JSON response of the response interface
|
||||
func writeResponse(w http.ResponseWriter, response any) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// handleError prints the REST method and error
|
||||
func handleError(method string, err error) {
|
||||
log.Errorf(log.APIServerMgr, "RESTful %s: handler failed to send JSON response. Error %s\n",
|
||||
method, err)
|
||||
}
|
||||
|
||||
// restGetAllSettings replies to a request with an encoded JSON response about the
|
||||
// trading Bots configuration.
|
||||
func (m *apiServerManager) restGetAllSettings(w http.ResponseWriter, r *http.Request) {
|
||||
err := writeResponse(w, config.GetConfig())
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// restSaveAllSettings saves all current settings from request body as a JSON
|
||||
// document then reloads state and returns the settings
|
||||
func (m *apiServerManager) restSaveAllSettings(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the data from the request
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var responseData config.Post
|
||||
err := decoder.Decode(&responseData)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
// Save change the settings
|
||||
cfg := config.GetConfig()
|
||||
err = cfg.UpdateConfig(m.gctConfigPath, &responseData.Data, false)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
|
||||
err = writeResponse(w, cfg)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
err = m.bot.SetupExchanges()
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// restGetAllActiveOrderbooks returns all enabled exchange orderbooks
|
||||
func (m *apiServerManager) restGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) {
|
||||
var response AllEnabledExchangeOrderbooks
|
||||
response.Data = getAllActiveOrderbooks(m.exchangeManager)
|
||||
err := writeResponse(w, response)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// restGetPortfolio returns the Bot portfolio manager
|
||||
func (m *apiServerManager) restGetPortfolio(w http.ResponseWriter, r *http.Request) {
|
||||
result := m.portfolioManager.GetPortfolioSummary()
|
||||
err := writeResponse(w, result)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// restGetAllActiveTickers returns all active tickers
|
||||
func (m *apiServerManager) restGetAllActiveTickers(w http.ResponseWriter, r *http.Request) {
|
||||
var response AllEnabledExchangeCurrencies
|
||||
response.Data = getAllActiveTickers(m.exchangeManager)
|
||||
err := writeResponse(w, response)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// restGetAllEnabledAccountInfo via get request returns JSON response of account
|
||||
// info
|
||||
func (m *apiServerManager) restGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
response := getAllActiveAccounts(m.exchangeManager)
|
||||
err := writeResponse(w, response)
|
||||
if err != nil {
|
||||
handleError(r.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// getIndex returns an HTML snippet for when a user requests the index URL
|
||||
func (m *apiServerManager) getIndex(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := fmt.Fprint(w, restIndexResponse)
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// getAllActiveOrderbooks returns all enabled exchanges orderbooks
|
||||
func getAllActiveOrderbooks(m iExchangeManager) []EnabledExchangeOrderbooks {
|
||||
exchanges, err := m.GetExchanges()
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Cannot get exchanges: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
orderbookData := make([]EnabledExchangeOrderbooks, 0, len(exchanges))
|
||||
for _, e := range exchanges {
|
||||
var orderbooks []orderbook.Book
|
||||
for _, a := range e.GetAssetTypes(true) {
|
||||
pairs, err := e.GetEnabledPairs(a)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Exchange %s could not retrieve enabled currencies. Err: %s\n", e.GetName(), err)
|
||||
continue
|
||||
}
|
||||
for _, pair := range pairs {
|
||||
ob, err := e.GetCachedOrderbook(pair, a)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Exchange %s failed to retrieve %s orderbook. Err: %s\n", e.GetName(), pair, err)
|
||||
continue
|
||||
}
|
||||
orderbooks = append(orderbooks, *ob)
|
||||
}
|
||||
}
|
||||
orderbookData = append(orderbookData, EnabledExchangeOrderbooks{ExchangeName: e.GetName(), ExchangeValues: orderbooks})
|
||||
}
|
||||
return orderbookData
|
||||
}
|
||||
|
||||
// getAllActiveTickers returns all enabled exchanges tickers
|
||||
func getAllActiveTickers(m iExchangeManager) []EnabledExchangeCurrencies {
|
||||
exchanges, err := m.GetExchanges()
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Cannot get exchanges: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
exchangeTickers := make([]EnabledExchangeCurrencies, 0, len(exchanges))
|
||||
for _, e := range exchanges {
|
||||
var tickers []*ticker.Price
|
||||
for _, a := range e.GetAssetTypes(true) {
|
||||
pairs, err := e.GetEnabledPairs(a)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Exchange %s could not retrieve enabled currencies. Err: %s\n", e.GetName(), err)
|
||||
continue
|
||||
}
|
||||
for _, pair := range pairs {
|
||||
t, err := e.GetCachedTicker(pair, a)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Exchange %s failed to retrieve %s ticker. Err: %s\n", e.GetName(), pair.String(), err)
|
||||
continue
|
||||
}
|
||||
tickers = append(tickers, t)
|
||||
}
|
||||
}
|
||||
exchangeTickers = append(exchangeTickers, EnabledExchangeCurrencies{ExchangeName: e.GetName(), ExchangeValues: tickers})
|
||||
}
|
||||
return exchangeTickers
|
||||
}
|
||||
|
||||
// getAllActiveAccounts returns all enabled exchanges accounts
|
||||
func getAllActiveAccounts(m iExchangeManager) []AllEnabledExchangeAccounts {
|
||||
exchanges, err := m.GetExchanges()
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "Cannot get exchanges: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
accounts := make([]AllEnabledExchangeAccounts, 0, len(exchanges))
|
||||
for x := range exchanges {
|
||||
assets := exchanges[x].GetAssetTypes(true)
|
||||
exchName := exchanges[x].GetName()
|
||||
var exchangeAccounts AllEnabledExchangeAccounts
|
||||
for y := range assets {
|
||||
a, err := exchanges[x].GetCachedAccountInfo(context.TODO(), assets[y])
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr,
|
||||
"Exchange %s failed to retrieve %s ticker. Err: %s\n",
|
||||
exchName,
|
||||
assets[y],
|
||||
err)
|
||||
continue
|
||||
}
|
||||
exchangeAccounts.Data = append(exchangeAccounts.Data, a)
|
||||
}
|
||||
accounts = append(accounts, exchangeAccounts)
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
|
||||
// StartWebsocketServer starts a Websocket handler
|
||||
func (m *apiServerManager) StartWebsocketServer() error {
|
||||
if !atomic.CompareAndSwapInt32(&m.websocketStarted, 0, 1) {
|
||||
return fmt.Errorf("websocket server %w", errAlreadyRunning)
|
||||
}
|
||||
if !m.remoteConfig.WebsocketRPC.Enabled {
|
||||
atomic.StoreInt32(&m.websocketStarted, 0)
|
||||
return fmt.Errorf("websocket %w", errServerDisabled)
|
||||
}
|
||||
log.Debugf(log.APIServerMgr,
|
||||
"Websocket RPC support enabled. Listen URL: ws://%s:%d/ws\n",
|
||||
common.ExtractHostOrDefault(m.websocketListenAddress),
|
||||
common.ExtractPortOrDefault(m.websocketListenAddress),
|
||||
)
|
||||
m.websocketRouter = m.newRouter(false)
|
||||
if m.websocketHTTPServer == nil {
|
||||
m.websocketHTTPServer = &http.Server{
|
||||
Addr: m.websocketListenAddress,
|
||||
Handler: m.websocketRouter,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
m.wgWebsocket.Go(func() {
|
||||
err := m.websocketHTTPServer.ListenAndServe()
|
||||
if err != nil {
|
||||
atomic.StoreInt32(&m.websocketStarted, 0)
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// newWebsocketHub Creates a new websocket hub
|
||||
func newWebsocketHub() *websocketHub {
|
||||
return &websocketHub{
|
||||
Broadcast: make(chan []byte),
|
||||
Register: make(chan *websocketClient),
|
||||
Unregister: make(chan *websocketClient),
|
||||
Clients: make(map[*websocketClient]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *websocketHub) run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-h.Register:
|
||||
h.Clients[client] = true
|
||||
case client := <-h.Unregister:
|
||||
if _, ok := h.Clients[client]; ok {
|
||||
log.Debugln(log.APIServerMgr, "websocket: disconnected client")
|
||||
delete(h.Clients, client)
|
||||
close(client.Send)
|
||||
}
|
||||
case message := <-h.Broadcast:
|
||||
for client := range h.Clients {
|
||||
select {
|
||||
case client.Send <- message:
|
||||
default:
|
||||
log.Debugln(log.APIServerMgr, "websocket: disconnected client")
|
||||
close(client.Send)
|
||||
delete(h.Clients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendWebsocketMessage sends a websocket event to the client
|
||||
func (c *websocketClient) SendWebsocketMessage(evt any) error {
|
||||
data, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "websocket: failed to send message: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Send <- data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketClient) read() {
|
||||
defer func() {
|
||||
c.Hub.Unregister <- c
|
||||
conErr := c.Conn.Close()
|
||||
if conErr != nil {
|
||||
log.Errorln(log.APIServerMgr, conErr)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
msgType, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
if gws.IsUnexpectedCloseError(err, gws.CloseGoingAway, gws.CloseAbnormalClosure) {
|
||||
log.Errorf(log.APIServerMgr, "websocket: client disconnected, err: %s\n", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if msgType == gws.TextMessage {
|
||||
var evt WebsocketEvent
|
||||
err := json.Unmarshal(message, &evt)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "websocket: failed to decode JSON sent from client %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if evt.Event == "" {
|
||||
log.Warnln(log.APIServerMgr, "websocket: client sent a blank event, disconnecting")
|
||||
continue
|
||||
}
|
||||
|
||||
dataJSON, err := json.Marshal(evt.Data)
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, "websocket: client sent data we couldn't JSON decode")
|
||||
break
|
||||
}
|
||||
|
||||
req := strings.ToLower(evt.Event)
|
||||
log.Debugf(log.APIServerMgr, "websocket: request received: %s\n", req)
|
||||
|
||||
result, ok := wsHandlers[req]
|
||||
if !ok {
|
||||
log.Debugln(log.APIServerMgr, "websocket: unsupported event")
|
||||
continue
|
||||
}
|
||||
|
||||
if result.authRequired && !c.Authenticated {
|
||||
log.Warnf(log.APIServerMgr, "Websocket: request %s failed due to unauthenticated request on an authenticated API\n", evt.Event)
|
||||
err = c.SendWebsocketMessage(WebsocketEventResponse{Event: evt.Event, Error: "unauthorised request on authenticated API"})
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
err = result.handler(c, dataJSON)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "websocket: request %s failed. Error %s\n", evt.Event, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *websocketClient) write() {
|
||||
defer func() {
|
||||
err := c.Conn.Close()
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
message, ok := <-c.Send
|
||||
if !ok {
|
||||
err := c.Conn.WriteMessage(gws.CloseMessage, []byte{})
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
log.Debugln(log.APIServerMgr, "websocket: hub closed the channel")
|
||||
return
|
||||
}
|
||||
|
||||
w, err := c.Conn.NextWriter(gws.TextMessage)
|
||||
if err != nil {
|
||||
log.Errorf(log.APIServerMgr, "websocket: failed to create new io.writeCloser: %s\n", err)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(message)
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
|
||||
// Add queued chat messages to the current websocket message
|
||||
n := len(c.Send)
|
||||
for range n {
|
||||
_, err = w.Write(<-c.Send)
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
log.Errorf(log.APIServerMgr, "websocket: failed to close io.WriteCloser: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StartWebsocketHandler starts the websocket hub and routine which
|
||||
// handles clients
|
||||
func StartWebsocketHandler() {
|
||||
if !wsHubStarted {
|
||||
wsHubStarted = true
|
||||
wsHub = newWebsocketHub()
|
||||
go wsHub.run()
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastWebsocketMessage meow
|
||||
func BroadcastWebsocketMessage(evt WebsocketEvent) error {
|
||||
if !wsHubStarted {
|
||||
return ErrWebsocketServiceNotRunning
|
||||
}
|
||||
|
||||
data, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wsHub.Broadcast <- data
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebsocketClientHandler upgrades the HTTP connection to a websocket
|
||||
// compatible one
|
||||
func (m *apiServerManager) WebsocketClientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !wsHubStarted {
|
||||
StartWebsocketHandler()
|
||||
}
|
||||
|
||||
connectionLimit := m.remoteConfig.WebsocketRPC.ConnectionLimit
|
||||
numClients := len(wsHub.Clients)
|
||||
|
||||
if numClients >= connectionLimit {
|
||||
log.Warnf(log.APIServerMgr,
|
||||
"websocket: client rejected due to websocket client limit reached. Number of clients %d. Limit %d.\n",
|
||||
numClients, connectionLimit)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
upgrader := gws.Upgrader{
|
||||
WriteBufferSize: 1024,
|
||||
ReadBufferSize: 1024,
|
||||
}
|
||||
|
||||
// Allow insecure origin if the Origin request header is present and not
|
||||
// equal to the Host request header. Default to false
|
||||
if m.remoteConfig.WebsocketRPC.AllowInsecureOrigin {
|
||||
upgrader.CheckOrigin = func(*http.Request) bool { return true }
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Errorln(log.APIServerMgr, err)
|
||||
return
|
||||
}
|
||||
|
||||
client := &websocketClient{
|
||||
Hub: wsHub,
|
||||
Conn: conn,
|
||||
Send: make(chan []byte, 1024),
|
||||
maxAuthFailures: m.remoteConfig.WebsocketRPC.MaxAuthFailures,
|
||||
username: m.remoteConfig.Username,
|
||||
password: m.remoteConfig.Password,
|
||||
configPath: m.gctConfigPath,
|
||||
exchangeManager: m.exchangeManager,
|
||||
bot: m.bot,
|
||||
portfolioManager: m.portfolioManager,
|
||||
}
|
||||
|
||||
client.Hub.Register <- client
|
||||
log.Debugf(log.APIServerMgr,
|
||||
"websocket: client connected. Connected clients: %d. Limit %d.\n",
|
||||
numClients+1, connectionLimit)
|
||||
|
||||
go client.read()
|
||||
go client.write()
|
||||
}
|
||||
|
||||
func wsAuth(client *websocketClient, data any) error {
|
||||
d, ok := data.([]byte)
|
||||
if !ok {
|
||||
return common.GetTypeAssertError("[]byte", data)
|
||||
}
|
||||
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "auth",
|
||||
}
|
||||
|
||||
var auth WebsocketAuth
|
||||
err := json.Unmarshal(d, &auth)
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
shasum := sha256.Sum256([]byte(client.password))
|
||||
if auth.Username == client.username && auth.Password == hex.EncodeToString(shasum[:]) {
|
||||
client.Authenticated = true
|
||||
wsResp.Data = WebsocketResponseSuccess
|
||||
log.Debugln(log.APIServerMgr,
|
||||
"websocket: client authenticated successfully")
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
wsResp.Error = "invalid username/password"
|
||||
client.authFailures++
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
if client.authFailures >= client.maxAuthFailures {
|
||||
log.Debugf(log.APIServerMgr,
|
||||
"websocket: disconnecting client, maximum auth failures threshold reached (failures: %d limit: %d)\n",
|
||||
client.authFailures, client.maxAuthFailures)
|
||||
wsHub.Unregister <- client
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf(log.APIServerMgr,
|
||||
"websocket: client sent wrong username/password (failures: %d limit: %d)\n",
|
||||
client.authFailures, client.maxAuthFailures)
|
||||
return nil
|
||||
}
|
||||
|
||||
func wsGetConfig(client *websocketClient, _ any) error {
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetConfig",
|
||||
Data: config.GetConfig(),
|
||||
}
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsSaveConfig(client *websocketClient, data any) error {
|
||||
d, ok := data.([]byte)
|
||||
if !ok {
|
||||
return common.GetTypeAssertError("[]byte", data)
|
||||
}
|
||||
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "SaveConfig",
|
||||
}
|
||||
var respCfg config.Config
|
||||
err := json.Unmarshal(d, &respCfg)
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := config.GetConfig()
|
||||
err = cfg.UpdateConfig(client.configPath, &respCfg, false)
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.bot.SetupExchanges()
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
wsResp.Data = WebsocketResponseSuccess
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsGetAccountInfo(client *websocketClient, _ any) error {
|
||||
accountInfo := getAllActiveAccounts(client.exchangeManager)
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetAccountInfo",
|
||||
Data: accountInfo,
|
||||
}
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsGetTickers(client *websocketClient, _ any) error {
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetTickers",
|
||||
}
|
||||
wsResp.Data = getAllActiveTickers(client.exchangeManager)
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsGetTicker(client *websocketClient, data any) error {
|
||||
d, ok := data.([]byte)
|
||||
if !ok {
|
||||
return common.GetTypeAssertError("[]byte", data)
|
||||
}
|
||||
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetTicker",
|
||||
}
|
||||
var tickerReq WebsocketOrderbookTickerRequest
|
||||
err := json.Unmarshal(d, &tickerReq)
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := currency.NewPairFromString(tickerReq.Currency)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err := asset.New(tickerReq.AssetType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exch, err := client.exchangeManager.GetExchangeByName(tickerReq.Exchange)
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
tick, err := exch.GetCachedTicker(p, a)
|
||||
if err != nil {
|
||||
wsResp.Error = err.Error()
|
||||
sendErr := client.SendWebsocketMessage(wsResp)
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
wsResp.Data = tick
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsGetOrderbooks(client *websocketClient, _ any) error {
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetOrderbooks",
|
||||
}
|
||||
wsResp.Data = getAllActiveOrderbooks(client.exchangeManager)
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsGetOrderbook(client *websocketClient, data any) error {
|
||||
d, ok := data.([]byte)
|
||||
if !ok {
|
||||
return common.GetTypeAssertError("[]byte", data)
|
||||
}
|
||||
|
||||
var orderbookReq WebsocketOrderbookTickerRequest
|
||||
err := json.Unmarshal(d, &orderbookReq)
|
||||
if err != nil {
|
||||
sendErr := client.SendWebsocketMessage(WebsocketEventResponse{Event: "GetOrderbook", Error: err.Error()})
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := currency.NewPairFromString(orderbookReq.Currency)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err := asset.New(orderbookReq.AssetType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exch, err := client.exchangeManager.GetExchangeByName(orderbookReq.Exchange)
|
||||
if err != nil {
|
||||
sendErr := client.SendWebsocketMessage(WebsocketEventResponse{Event: "GetOrderbook", Error: err.Error()})
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
ob, err := exch.GetCachedOrderbook(p, a)
|
||||
if err != nil {
|
||||
sendErr := client.SendWebsocketMessage(WebsocketEventResponse{Event: "GetOrderbook", Error: err.Error()})
|
||||
if sendErr != nil {
|
||||
log.Errorln(log.APIServerMgr, sendErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return client.SendWebsocketMessage(WebsocketEventResponse{Event: "GetOrderbook", Data: ob})
|
||||
}
|
||||
|
||||
func wsGetExchangeRates(client *websocketClient, _ any) error {
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetExchangeRates",
|
||||
}
|
||||
|
||||
var err error
|
||||
wsResp.Data, err = currency.GetExchangeRates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
|
||||
func wsGetPortfolio(client *websocketClient, _ any) error {
|
||||
wsResp := WebsocketEventResponse{
|
||||
Event: "GetPortfolio",
|
||||
}
|
||||
|
||||
wsResp.Data = client.portfolioManager.GetPortfolioSummary()
|
||||
return client.SendWebsocketMessage(wsResp)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
# GoCryptoTrader package Apiserver
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/apiserver)
|
||||
[](https://codecov.io/gh/thrasher-corp/gocryptotrader)
|
||||
[](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
|
||||
|
||||
|
||||
This apiserver package is part of the GoCryptoTrader codebase.
|
||||
|
||||
## This is still in active development
|
||||
|
||||
You can track ideas, planned features and what's in progress on our [GoCryptoTrader Kanban board](https://github.com/orgs/thrasher-corp/projects/3).
|
||||
|
||||
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/zt-38z8abs3l-gH8AAOk8XND6DP5NfCiG_g)
|
||||
|
||||
## Current Features for Apiserver
|
||||
+ The API server subsystem is a deprecated service used to host a REST or websocket server to interact with some functions of GoCryptoTrader
|
||||
+ This subsystem is no longer maintained and it is highly encouraged to interact with GRPC endpoints directly where possible
|
||||
+ In order to modify the behaviour of the API server subsystem, you can edit the following inside your config file:
|
||||
|
||||
### deprecatedRPC
|
||||
|
||||
| Config | Description | Example |
|
||||
| ------ | ----------- | ------- |
|
||||
| enabled | If enabled will create a REST server which will listen to commands on the listen address | `true` |
|
||||
| listenAddress | If enabled will listen for REST requests on this address and return a JSON response | `localhost:9050` |
|
||||
|
||||
### websocketRPC
|
||||
|
||||
| Config | Description | Example |
|
||||
| ------ | ----------- | ------- |
|
||||
| enabled | If enabled will create a REST server which will listen to commands on the listen address | `true` |
|
||||
| listenAddress | If enabled will listen for requests on this address and return a JSON response | `localhost:9051` |
|
||||
| connectionLimit | Defines how many connections the websocket RPC server can handle simultanesoly | `1` |
|
||||
| maxAuthFailures | For authenticated endpoints, the amount of failed attempts allowed before disconnection | `3` |
|
||||
| allowInsecureOrigin | Allows use of insecure connections | `true` |
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
|
||||
@@ -1,224 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/encoding/json"
|
||||
)
|
||||
|
||||
func TestSetupAPIServerManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := setupAPIServerManager(nil, nil, nil, nil, nil, "")
|
||||
assert.ErrorIs(t, err, errNilRemoteConfig)
|
||||
|
||||
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, nil, nil, nil, nil, "")
|
||||
assert.ErrorIs(t, err, errNilPProfConfig)
|
||||
|
||||
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, nil, nil, nil, "")
|
||||
assert.ErrorIs(t, err, errNilExchangeManager)
|
||||
|
||||
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, nil, nil, "")
|
||||
assert.ErrorIs(t, err, errNilBot)
|
||||
|
||||
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, "")
|
||||
assert.ErrorIs(t, err, errEmptyConfigPath)
|
||||
|
||||
wd, _ := os.Getwd()
|
||||
_, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStartRESTServer(t *testing.T) {
|
||||
t.Parallel()
|
||||
wd, _ := os.Getwd()
|
||||
m, err := setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StartRESTServer()
|
||||
assert.ErrorIs(t, err, errServerDisabled)
|
||||
|
||||
m.remoteConfig.DeprecatedRPC.Enabled = true
|
||||
err = m.StartRESTServer()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStartWebsocketServer(t *testing.T) {
|
||||
t.Parallel()
|
||||
wd, _ := os.Getwd()
|
||||
m, err := setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StartWebsocketServer()
|
||||
assert.ErrorIs(t, err, errServerDisabled)
|
||||
|
||||
m.remoteConfig.WebsocketRPC.Enabled = true
|
||||
err = m.StartWebsocketServer()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStopRESTServer(t *testing.T) {
|
||||
t.Parallel()
|
||||
wd, _ := os.Getwd()
|
||||
m, err := setupAPIServerManager(&config.RemoteControlConfig{
|
||||
DeprecatedRPC: config.DepcrecatedRPCConfig{
|
||||
Enabled: true,
|
||||
ListenAddress: "localhost:9051",
|
||||
},
|
||||
}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StopRESTServer()
|
||||
assert.ErrorIs(t, err, ErrSubSystemNotStarted)
|
||||
|
||||
err = m.StartRESTServer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StopRESTServer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// do it again to ensure things have reset appropriately and no errors occur starting
|
||||
err = m.StartRESTServer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StopRESTServer()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebsocketStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
wd, _ := os.Getwd()
|
||||
m, err := setupAPIServerManager(&config.RemoteControlConfig{
|
||||
WebsocketRPC: config.WebsocketRPCConfig{
|
||||
Enabled: true,
|
||||
ListenAddress: "localhost:9052",
|
||||
},
|
||||
}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StopWebsocketServer()
|
||||
assert.ErrorIs(t, err, ErrSubSystemNotStarted)
|
||||
|
||||
err = m.StartWebsocketServer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StopWebsocketServer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// do it again to ensure things have reset appropriately and no errors occur starting
|
||||
err = m.StartWebsocketServer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = m.StopWebsocketServer()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIsRESTServerRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := &apiServerManager{}
|
||||
assert.False(t, m.IsRESTServerRunning(), "should return correctly with empty type")
|
||||
m.restStarted = 1
|
||||
assert.True(t, m.IsRESTServerRunning(), "should return correctly with restStarted set")
|
||||
assert.False(t, (*apiServerManager)(nil).IsRESTServerRunning(), "should return correctly on nil type")
|
||||
}
|
||||
|
||||
func TestIsWebsocketServerRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := &apiServerManager{}
|
||||
assert.False(t, m.IsWebsocketServerRunning(), "should return correctly with empty type")
|
||||
m.websocketStarted = 1
|
||||
assert.True(t, m.IsWebsocketServerRunning(), "should return correctly with websocketStarted set")
|
||||
assert.False(t, (*apiServerManager)(nil).IsWebsocketServerRunning(), "should return correctly on nil type")
|
||||
}
|
||||
|
||||
func TestGetAllActiveOrderbooks(t *testing.T) {
|
||||
man := NewExchangeManager()
|
||||
bs, err := man.NewExchangeByName("Bitstamp")
|
||||
require.NoError(t, err, "NewExchangeByName must not error")
|
||||
bs.SetDefaults()
|
||||
err = man.Add(bs)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := getAllActiveOrderbooks(man)
|
||||
assert.NotNil(t, resp)
|
||||
}
|
||||
|
||||
func TestGetAllActiveTickers(t *testing.T) {
|
||||
t.Parallel()
|
||||
man := NewExchangeManager()
|
||||
bs, err := man.NewExchangeByName("Bitstamp")
|
||||
require.NoError(t, err, "NewExchangeByName must not error")
|
||||
bs.SetDefaults()
|
||||
err = man.Add(bs)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := getAllActiveTickers(man)
|
||||
assert.NotNil(t, resp)
|
||||
}
|
||||
|
||||
func TestGetAllActiveAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
man := NewExchangeManager()
|
||||
bs, err := man.NewExchangeByName("Bitstamp")
|
||||
require.NoError(t, err, "NewExchangeByName must not error")
|
||||
bs.SetDefaults()
|
||||
err = man.Add(bs)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := getAllActiveAccounts(man)
|
||||
assert.NotNil(t, resp)
|
||||
}
|
||||
|
||||
func makeHTTPGetRequest(t *testing.T, response any) *http.Response {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
err := writeResponse(w, response)
|
||||
require.NoError(t, err)
|
||||
|
||||
return w.Result()
|
||||
}
|
||||
|
||||
// TestConfigAllJsonResponse test if config/all restful json response is valid
|
||||
func TestConfigAllJsonResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
var c config.Config
|
||||
err := c.LoadConfig(config.TestFile, true)
|
||||
assert.NoError(t, err, "LoadConfig should not error")
|
||||
|
||||
resp := makeHTTPGetRequest(t, c)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "ReadAll should not error")
|
||||
err = resp.Body.Close()
|
||||
assert.NoError(t, err, "Close body should not error")
|
||||
|
||||
var responseConfig config.Config
|
||||
err = json.Unmarshal(body, &responseConfig)
|
||||
assert.NoError(t, err, "Unmarshal should not error")
|
||||
for i, e := range responseConfig.Exchanges {
|
||||
err = e.CurrencyPairs.SetDelimitersFromConfig()
|
||||
assert.NoError(t, err, "SetDelimitersFromConfig should not error")
|
||||
// Using require here makes it much easier to isolate differences per-exchange than below
|
||||
// We look into pointers separately
|
||||
for a, p := range e.CurrencyPairs.Pairs {
|
||||
require.Equalf(t, c.Exchanges[i].CurrencyPairs.Pairs[a], p, "%s exchange Config CurrencyManager Pairs for asset %s must match api response", e.Name, a)
|
||||
}
|
||||
require.Equalf(t, c.Exchanges[i].CurrencyPairs, e.CurrencyPairs, "%s exchange Config CurrencyManager must match api response", e.Name)
|
||||
require.Equalf(t, c.Exchanges[i], e, "%s exchange Config must match api response", e.Name) // require here makes it much easier to isolate differences than below
|
||||
}
|
||||
assert.Equal(t, c, responseConfig, "Config should match api response")
|
||||
}
|
||||
|
||||
// fakeBot is a basic implementation of the iBot interface used for testing
|
||||
type fakeBot struct{}
|
||||
|
||||
// SetupExchanges is a basic implementation of the iBot interface used for testing
|
||||
func (f *fakeBot) SetupExchanges() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
)
|
||||
|
||||
// Const vars for websocket
|
||||
const (
|
||||
WebsocketResponseSuccess = "OK"
|
||||
restIndexResponse = "<html>GoCryptoTrader RESTful interface. For the web GUI, please visit the <a href=https://github.com/thrasher-corp/gocryptotrader/blob/master/web/README.md>web GUI readme.</a></html>"
|
||||
DeprecatedName = "deprecated_rpc"
|
||||
WebsocketName = "websocket_rpc"
|
||||
)
|
||||
|
||||
var (
|
||||
wsHub *websocketHub
|
||||
wsHubStarted bool
|
||||
errNilRemoteConfig = errors.New("received nil remote config")
|
||||
errNilPProfConfig = errors.New("received nil pprof config")
|
||||
errNilBot = errors.New("received nil engine bot")
|
||||
errEmptyConfigPath = errors.New("received empty config path")
|
||||
errServerDisabled = errors.New("server disabled")
|
||||
errAlreadyRunning = errors.New("already running")
|
||||
// ErrWebsocketServiceNotRunning occurs when a message is sent to be broadcast via websocket
|
||||
// and its not running
|
||||
ErrWebsocketServiceNotRunning = errors.New("websocket service not started")
|
||||
)
|
||||
|
||||
// apiServerManager holds all relevant fields to manage both REST and websocket
|
||||
// api servers
|
||||
type apiServerManager struct {
|
||||
restStarted int32
|
||||
websocketStarted int32
|
||||
restListenAddress string
|
||||
websocketListenAddress string
|
||||
gctConfigPath string
|
||||
restHTTPServer *http.Server
|
||||
websocketHTTPServer *http.Server
|
||||
wgRest sync.WaitGroup
|
||||
wgWebsocket sync.WaitGroup
|
||||
|
||||
restRouter *mux.Router
|
||||
websocketRouter *mux.Router
|
||||
websocketHub *websocketHub
|
||||
|
||||
remoteConfig *config.RemoteControlConfig
|
||||
pprofConfig *config.Profiler
|
||||
exchangeManager iExchangeManager
|
||||
bot iBot
|
||||
portfolioManager iPortfolioManager
|
||||
}
|
||||
|
||||
// websocketClient stores information related to the websocket client
|
||||
type websocketClient struct {
|
||||
Hub *websocketHub
|
||||
Conn *gws.Conn
|
||||
Authenticated bool
|
||||
authFailures int
|
||||
Send chan []byte
|
||||
username string
|
||||
password string
|
||||
maxAuthFailures int
|
||||
exchangeManager iExchangeManager
|
||||
bot iBot
|
||||
portfolioManager iPortfolioManager
|
||||
configPath string
|
||||
}
|
||||
|
||||
// websocketHub stores the data for managing websocket clients
|
||||
type websocketHub struct {
|
||||
Clients map[*websocketClient]bool
|
||||
Broadcast chan []byte
|
||||
Register chan *websocketClient
|
||||
Unregister chan *websocketClient
|
||||
}
|
||||
|
||||
// WebsocketEvent is the struct used for websocket events
|
||||
type WebsocketEvent struct {
|
||||
Exchange string `json:"exchange,omitempty"`
|
||||
AssetType string `json:"assetType,omitempty"`
|
||||
Event string
|
||||
Data any
|
||||
}
|
||||
|
||||
// WebsocketEventResponse is the struct used for websocket event responses
|
||||
type WebsocketEventResponse struct {
|
||||
Event string `json:"event"`
|
||||
Data any `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook
|
||||
// requests
|
||||
type WebsocketOrderbookTickerRequest struct {
|
||||
Exchange string `json:"exchangeName"`
|
||||
Currency string `json:"currency"`
|
||||
AssetType string `json:"assetType"`
|
||||
}
|
||||
|
||||
// WebsocketAuth is a struct used for
|
||||
type WebsocketAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Route is a sub type that holds the request routes
|
||||
type Route struct {
|
||||
Name string
|
||||
Method string
|
||||
Pattern string
|
||||
HandlerFunc http.HandlerFunc
|
||||
}
|
||||
|
||||
// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks
|
||||
type AllEnabledExchangeOrderbooks struct {
|
||||
Data []EnabledExchangeOrderbooks `json:"data"`
|
||||
}
|
||||
|
||||
// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective
|
||||
// orderbooks
|
||||
type EnabledExchangeOrderbooks struct {
|
||||
ExchangeName string `json:"exchangeName"`
|
||||
ExchangeValues []orderbook.Book `json:"exchangeValues"`
|
||||
}
|
||||
|
||||
// AllEnabledExchangeCurrencies holds the enabled exchange currencies
|
||||
type AllEnabledExchangeCurrencies struct {
|
||||
Data []EnabledExchangeCurrencies `json:"data"`
|
||||
}
|
||||
|
||||
// EnabledExchangeCurrencies is a sub type for singular exchanges and respective
|
||||
// currencies
|
||||
type EnabledExchangeCurrencies struct {
|
||||
ExchangeName string `json:"exchangeName"`
|
||||
ExchangeValues []*ticker.Price `json:"exchangeValues"`
|
||||
}
|
||||
|
||||
// AllEnabledExchangeAccounts holds all enabled accounts info
|
||||
type AllEnabledExchangeAccounts struct {
|
||||
Data []account.Holdings `json:"data"`
|
||||
}
|
||||
|
||||
var wsHandlers = map[string]wsCommandHandler{
|
||||
"auth": {authRequired: false, handler: wsAuth},
|
||||
"getconfig": {authRequired: true, handler: wsGetConfig},
|
||||
"saveconfig": {authRequired: true, handler: wsSaveConfig},
|
||||
"getaccountinfo": {authRequired: true, handler: wsGetAccountInfo},
|
||||
"gettickers": {authRequired: false, handler: wsGetTickers},
|
||||
"getticker": {authRequired: false, handler: wsGetTicker},
|
||||
"getorderbooks": {authRequired: false, handler: wsGetOrderbooks},
|
||||
"getorderbook": {authRequired: false, handler: wsGetOrderbook},
|
||||
"getexchangerates": {authRequired: false, handler: wsGetExchangeRates},
|
||||
"getportfolio": {authRequired: true, handler: wsGetPortfolio},
|
||||
}
|
||||
|
||||
type wsCommandHandler struct {
|
||||
authRequired bool
|
||||
handler func(client *websocketClient, data any) error
|
||||
}
|
||||
@@ -68,7 +68,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -17,20 +17,20 @@ This currency_state_manager package is part of the GoCryptoTrader codebase.
|
||||
You can track ideas, planned features and what's in progress on our [GoCryptoTrader Kanban board](https://github.com/orgs/thrasher-corp/projects/3).
|
||||
|
||||
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/zt-38z8abs3l-gH8AAOk8XND6DP5NfCiG_g)
|
||||
|
||||
## Current Features for Currency State Manager
|
||||
+ The state manager keeps currency states up to date, which include:
|
||||
* Withdrawal - Determines if the currency is allowed to be withdrawn from the exchange.
|
||||
* Deposit - Determines if the currency is allowed to be deposited to an exchange.
|
||||
* Trading - Determines if the currency is allowed to be traded on the exchange.
|
||||
|
||||
+ This allows for an internal state check to compliment internal and external
|
||||
strategies.
|
||||
|
||||
|
||||
## Current Features for Currency State Manager
|
||||
+ The state manager keeps currency states up to date, which include:
|
||||
* Withdrawal - Determines if the currency is allowed to be withdrawn from the exchange.
|
||||
* Deposit - Determines if the currency is allowed to be deposited to an exchange.
|
||||
* Trading - Determines if the currency is allowed to be traded on the exchange.
|
||||
|
||||
+ This allows for an internal state check to compliment internal and external
|
||||
strategies.
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
|
||||
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
|
||||
|
||||
@@ -44,7 +44,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ func (m *DataHistoryManager) runJobs() error {
|
||||
}
|
||||
|
||||
if !atomic.CompareAndSwapInt32(&m.processing, 0, 1) {
|
||||
return fmt.Errorf("cannot process jobs, %w", errAlreadyRunning)
|
||||
return fmt.Errorf("cannot process jobs, %w", ErrSubSystemAlreadyStarted)
|
||||
}
|
||||
defer atomic.StoreInt32(&m.processing, 0)
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ The candle table also has relationships to data history jobs. Only the relevant
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
// overarching type across this code base.
|
||||
type Engine struct {
|
||||
Config *config.Config
|
||||
apiServer *apiServerManager
|
||||
CommunicationsManager *CommunicationManager
|
||||
connectionManager *connectionManager
|
||||
currencyPairSyncer *SyncManager
|
||||
@@ -198,9 +197,6 @@ func validateSettings(b *Engine, s *Settings, flagSet FlagSet) {
|
||||
go b.waitForGPRCShutdown()
|
||||
}
|
||||
|
||||
flagSet.WithBool("websocketrpc", &b.Settings.EnableWebsocketRPC, b.Config.RemoteControl.WebsocketRPC.Enabled)
|
||||
flagSet.WithBool("deprecatedrpc", &b.Settings.EnableDeprecatedRPC, b.Config.RemoteControl.DeprecatedRPC.Enabled)
|
||||
|
||||
if flagSet["maxvirtualmachines"] {
|
||||
maxMachines := b.Settings.MaxVirtualMachines
|
||||
b.gctScriptManager.MaxVirtualMachines = &maxMachines
|
||||
@@ -300,6 +296,12 @@ func (bot *Engine) Start() error {
|
||||
newEngineMutex.Lock()
|
||||
defer newEngineMutex.Unlock()
|
||||
|
||||
if bot.Config.Profiler.Enabled {
|
||||
if err := StartPPROF(context.TODO(), &bot.Config.Profiler); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "Failed to start pprof: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if bot.Settings.EnableDatabaseManager {
|
||||
if d, err := SetupDatabaseConnectionManager(&bot.Config.Database); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "Database manager unable to setup: %v", err)
|
||||
@@ -439,28 +441,6 @@ func (bot *Engine) Start() error {
|
||||
bot.WithdrawManager = w
|
||||
}
|
||||
|
||||
if bot.Settings.EnableDeprecatedRPC || bot.Settings.EnableWebsocketRPC {
|
||||
if filePath, err := config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile); err != nil {
|
||||
return err
|
||||
} else { //nolint:revive // TODO: revive false positive, see https://github.com/mgechev/revive/pull/832 for more information
|
||||
if a, err := setupAPIServerManager(&bot.Config.RemoteControl, &bot.Config.Profiler, bot.ExchangeManager, bot, bot.portfolioManager, filePath); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "API Server unable to start: %s", err)
|
||||
} else {
|
||||
bot.apiServer = a
|
||||
if bot.Settings.EnableDeprecatedRPC {
|
||||
if err := bot.apiServer.StartRESTServer(); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "could not start REST API server: %s", err)
|
||||
}
|
||||
}
|
||||
if bot.Settings.EnableWebsocketRPC {
|
||||
if err := bot.apiServer.StartWebsocketServer(); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "could not start websocket API server: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bot.Settings.EnableDepositAddressManager {
|
||||
bot.DepositAddressManager = SetupDepositAddressManager()
|
||||
go func() {
|
||||
@@ -624,16 +604,6 @@ func (bot *Engine) Stop() {
|
||||
gctlog.Errorf(gctlog.Global, "Connection manager unable to stop. Error: %v", err)
|
||||
}
|
||||
}
|
||||
if bot.apiServer.IsRESTServerRunning() {
|
||||
if err := bot.apiServer.StopRESTServer(); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "API Server unable to stop REST server. Error: %s", err)
|
||||
}
|
||||
}
|
||||
if bot.apiServer.IsWebsocketServerRunning() {
|
||||
if err := bot.apiServer.StopWebsocketServer(); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "API Server unable to stop websocket server. Error: %s", err)
|
||||
}
|
||||
}
|
||||
if bot.dataHistoryManager.IsRunning() {
|
||||
if err := bot.dataHistoryManager.Stop(); err != nil {
|
||||
gctlog.Errorf(gctlog.DataHistory, "data history manager unable to stop. Error: %v", err)
|
||||
|
||||
@@ -41,8 +41,6 @@ type CoreSettings struct {
|
||||
EnableGRPC bool
|
||||
EnableGRPCProxy bool
|
||||
EnableGRPCShutdown bool
|
||||
EnableWebsocketRPC bool
|
||||
EnableDeprecatedRPC bool
|
||||
EnableCommsRelayer bool
|
||||
EnableExchangeSyncManager bool
|
||||
EnableDepositAddressManager bool
|
||||
|
||||
@@ -31,7 +31,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -12,8 +12,11 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -52,6 +55,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/okx"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/poloniex"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/yobit"
|
||||
"github.com/thrasher-corp/gocryptotrader/gctscript/vm"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -63,6 +67,11 @@ var (
|
||||
errCertTypeInvalid = errors.New("gRPC TLS certificate type is invalid")
|
||||
errSubsystemNotFound = errors.New("subsystem not found")
|
||||
errGRPCManagementFault = errors.New("cannot manage GRPC subsystem via GRPC. Please manually change your config")
|
||||
errNilBot = errors.New("received nil engine bot")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPPROFListenAddress = "localhost:8085"
|
||||
)
|
||||
|
||||
// GetSubsystemsStatus returns the status of various subsystems
|
||||
@@ -78,8 +87,6 @@ func (bot *Engine) GetSubsystemsStatus() map[string]bool {
|
||||
grpcName: bot.Settings.EnableGRPC,
|
||||
grpcProxyName: bot.Settings.EnableGRPCProxy,
|
||||
vm.Name: bot.gctScriptManager.IsRunning(),
|
||||
DeprecatedName: bot.Settings.EnableDeprecatedRPC,
|
||||
WebsocketName: bot.Settings.EnableWebsocketRPC,
|
||||
dispatch.Name: dispatch.IsRunning(),
|
||||
dataHistoryManagerName: bot.dataHistoryManager.IsRunning(),
|
||||
CurrencyStateManagementName: bot.currencyStateManager.IsRunning(),
|
||||
@@ -106,14 +113,6 @@ func (bot *Engine) GetRPCEndpoints() (map[string]RPCEndpoint, error) {
|
||||
Started: bot.Settings.EnableGRPCProxy,
|
||||
ListenAddr: "https://" + bot.Config.RemoteControl.GRPC.GRPCProxyListenAddress,
|
||||
},
|
||||
DeprecatedName: {
|
||||
Started: bot.Settings.EnableDeprecatedRPC,
|
||||
ListenAddr: "http://" + bot.Config.RemoteControl.DeprecatedRPC.ListenAddress,
|
||||
},
|
||||
WebsocketName: {
|
||||
Started: bot.Settings.EnableWebsocketRPC,
|
||||
ListenAddr: "ws://" + bot.Config.RemoteControl.WebsocketRPC.ListenAddress,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -237,38 +236,6 @@ func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error {
|
||||
return dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit)
|
||||
}
|
||||
return dispatch.Stop()
|
||||
case DeprecatedName:
|
||||
if enable {
|
||||
if bot.apiServer == nil {
|
||||
var filePath string
|
||||
filePath, err = config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bot.apiServer, err = setupAPIServerManager(&bot.Config.RemoteControl, &bot.Config.Profiler, bot.ExchangeManager, bot, bot.portfolioManager, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return bot.apiServer.StartRESTServer()
|
||||
}
|
||||
return bot.apiServer.StopRESTServer()
|
||||
case WebsocketName:
|
||||
if enable {
|
||||
if bot.apiServer == nil {
|
||||
var filePath string
|
||||
filePath, err = config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bot.apiServer, err = setupAPIServerManager(&bot.Config.RemoteControl, &bot.Config.Profiler, bot.ExchangeManager, bot, bot.portfolioManager, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return bot.apiServer.StartWebsocketServer()
|
||||
}
|
||||
return bot.apiServer.StopWebsocketServer()
|
||||
case grpcName, grpcProxyName:
|
||||
return errGRPCManagementFault
|
||||
case dataHistoryManagerName:
|
||||
@@ -807,6 +774,18 @@ func (bot *Engine) GetExchangeNames(enabledOnly bool) []string {
|
||||
return response
|
||||
}
|
||||
|
||||
// AllEnabledExchangeCurrencies holds the enabled exchange currencies
|
||||
type AllEnabledExchangeCurrencies struct {
|
||||
Data []EnabledExchangeCurrencies `json:"data"`
|
||||
}
|
||||
|
||||
// EnabledExchangeCurrencies is a sub type for singular exchanges and respective
|
||||
// currencies
|
||||
type EnabledExchangeCurrencies struct {
|
||||
ExchangeName string `json:"exchangeName"`
|
||||
ExchangeValues []*ticker.Price `json:"exchangeValues"`
|
||||
}
|
||||
|
||||
// GetAllActiveTickers returns all enabled exchange tickers
|
||||
func (bot *Engine) GetAllActiveTickers() []EnabledExchangeCurrencies {
|
||||
var tickerData []EnabledExchangeCurrencies
|
||||
@@ -1037,3 +1016,48 @@ func NewExchangeByNameWithDefaults(ctx context.Context, name string) (exchange.I
|
||||
}
|
||||
return exch, nil
|
||||
}
|
||||
|
||||
// StartPPROF starts a pprof profiler if enabled
|
||||
func StartPPROF(ctx context.Context, cfg *config.Profiler) error {
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
runtime.SetMutexProfileFraction(cfg.MutexProfileFraction)
|
||||
runtime.SetBlockProfileRate(cfg.BlockProfileRate)
|
||||
|
||||
listenAddr := cfg.ListenAddress
|
||||
if listenAddr == "" {
|
||||
listenAddr = defaultPPROFListenAddress
|
||||
}
|
||||
|
||||
lc := net.ListenConfig{}
|
||||
ln, err := lc.Listen(ctx, "tcp", listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pprof listen error: %w", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: listenAddr,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Infof(log.Global, "PPROF profiler listening on http://%s/debug/pprof/", listenAddr)
|
||||
|
||||
go func() {
|
||||
if err := srv.Serve(ln); err != nil {
|
||||
log.Errorf(log.Global, "PPROF serve error: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -99,10 +100,7 @@ func CreateTestBot(tb testing.TB) *Engine {
|
||||
}
|
||||
|
||||
func TestGetSubsystemsStatus(t *testing.T) {
|
||||
m := (&Engine{}).GetSubsystemsStatus()
|
||||
if len(m) != 15 {
|
||||
t.Fatalf("subsystem count is wrong expecting: %d but received: %d", 15, len(m))
|
||||
}
|
||||
assert.Len(t, (&Engine{}).GetSubsystemsStatus(), 13, "GetSubsystemStatus should return the correct number of subsystems")
|
||||
}
|
||||
|
||||
func TestGetRPCEndpoints(t *testing.T) {
|
||||
@@ -111,10 +109,7 @@ func TestGetRPCEndpoints(t *testing.T) {
|
||||
|
||||
m, err := (&Engine{Config: &config.Config{}}).GetRPCEndpoints()
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(m) != 4 {
|
||||
t.Fatalf("expected length: %d but received: %d", 4, len(m))
|
||||
}
|
||||
assert.Len(t, m, 2, "GetRPCEndpoints should return the correct number of RPC endpoints")
|
||||
}
|
||||
|
||||
func TestSetSubsystem(t *testing.T) { //nolint // TO-DO: Fix race t.Parallel() usage
|
||||
@@ -175,19 +170,6 @@ func TestSetSubsystem(t *testing.T) { //nolint // TO-DO: Fix race t.Parallel() u
|
||||
EnableError: nil,
|
||||
DisableError: nil,
|
||||
},
|
||||
|
||||
{
|
||||
Subsystem: DeprecatedName,
|
||||
Engine: &Engine{Config: &config.Config{}, Settings: Settings{ConfigFile: config.DefaultFilePath()}},
|
||||
EnableError: errServerDisabled,
|
||||
DisableError: ErrSubSystemNotStarted,
|
||||
},
|
||||
{
|
||||
Subsystem: WebsocketName,
|
||||
Engine: &Engine{Config: &config.Config{}, Settings: Settings{ConfigFile: config.DefaultFilePath()}},
|
||||
EnableError: errServerDisabled,
|
||||
DisableError: ErrSubSystemNotStarted,
|
||||
},
|
||||
{
|
||||
Subsystem: grpcName,
|
||||
Engine: &Engine{Config: &config.Config{}},
|
||||
@@ -1258,3 +1240,22 @@ func TestNewExchangeByNameWithDefaults(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPPROF(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.NoError(t, StartPPROF(t.Context(), &config.Profiler{Enabled: false}), "StartPPROF with a disabled config should not error")
|
||||
pprofConfig := &config.Profiler{
|
||||
Enabled: true,
|
||||
ListenAddress: "",
|
||||
MutexProfileFraction: 1,
|
||||
BlockProfileRate: 1,
|
||||
}
|
||||
require.NoError(t, StartPPROF(t.Context(), pprofConfig), "StartPPROF with a valid config must not error")
|
||||
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://localhost:8085/debug/pprof/mutex", http.NoBody)
|
||||
require.NoError(t, err, "NewRequestWithContext must not error")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err, "Do must not error")
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "Get response status must be OK")
|
||||
resp.Body.Close()
|
||||
assert.Error(t, StartPPROF(t.Context(), pprofConfig), "StartPPROF with a valid config on already used port should error")
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -68,11 +68,6 @@ type iPortfolioManager interface {
|
||||
IsExchangeSupported(string, string) bool
|
||||
}
|
||||
|
||||
// iBot limits exposure of accessible functions to engine bot
|
||||
type iBot interface {
|
||||
SetupExchanges() error
|
||||
}
|
||||
|
||||
// iCurrencyPairSyncer defines a limited scoped currency pair syncer
|
||||
type iCurrencyPairSyncer interface {
|
||||
IsRunning() bool
|
||||
|
||||
@@ -27,7 +27,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -593,11 +593,6 @@ func (m *SyncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan
|
||||
c.Key.Asset)
|
||||
}
|
||||
m.PrintTickerSummary(result, "REST", err)
|
||||
if err == nil {
|
||||
if m.remoteConfig.WebsocketRPC.Enabled {
|
||||
relayWebsocketEvent(result, "ticker_update", c.Key.Asset.String(), exchangeName)
|
||||
}
|
||||
}
|
||||
updateErr := m.update(c, SyncItemTicker, err)
|
||||
if updateErr != nil {
|
||||
log.Errorln(log.SyncMgr, updateErr)
|
||||
@@ -642,11 +637,6 @@ func (m *SyncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc
|
||||
c.Pair,
|
||||
c.Key.Asset)
|
||||
m.PrintOrderbookSummary(result, "REST", err)
|
||||
if err == nil {
|
||||
if m.remoteConfig.WebsocketRPC.Enabled {
|
||||
relayWebsocketEvent(result, "orderbook_update", c.Key.Asset.String(), e.GetName())
|
||||
}
|
||||
}
|
||||
updateErr := m.update(c, SyncItemOrderbook, err)
|
||||
if updateErr != nil {
|
||||
log.Errorln(log.SyncMgr, updateErr)
|
||||
@@ -887,20 +877,6 @@ func (m *SyncManager) WaitForInitialSync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func relayWebsocketEvent(result any, event, assetType, exchangeName string) {
|
||||
evt := WebsocketEvent{
|
||||
Data: result,
|
||||
Event: event,
|
||||
AssetType: assetType,
|
||||
Exchange: exchangeName,
|
||||
}
|
||||
err := BroadcastWebsocketMessage(evt)
|
||||
if err != nil && !errors.Is(err, ErrWebsocketServiceNotRunning) {
|
||||
log.Errorf(log.APIServerMgr, "Failed to broadcast websocket event %v. Error: %v",
|
||||
event, err)
|
||||
}
|
||||
}
|
||||
|
||||
func greatestCommonDivisor(a, b time.Duration) time.Duration {
|
||||
for b != 0 {
|
||||
t := b
|
||||
|
||||
@@ -35,7 +35,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -201,12 +201,6 @@ func TestPrintOrderbookSummary(t *testing.T) {
|
||||
m.PrintOrderbookSummary(nil, "REST", errors.New("test"))
|
||||
}
|
||||
|
||||
func TestRelayWebsocketEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
relayWebsocketEvent(nil, "", "", "")
|
||||
}
|
||||
|
||||
func TestWaitForInitialSync(t *testing.T) {
|
||||
var m *SyncManager
|
||||
err := m.WaitForInitialSync()
|
||||
|
||||
@@ -27,7 +27,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
<img src="/docs/assets/donate.png" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user