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:
Adrian Gallagher
2025-09-30 13:32:09 +10:00
committed by GitHub
parent 0b60693ff5
commit bb122dcafa
388 changed files with 360 additions and 23901 deletions

View File

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

View File

@@ -1,49 +0,0 @@
# GoCryptoTrader package Apiserver
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/apiserver)
[![Coverage Status](https://codecov.io/gh/thrasher-corp/gocryptotrader/graph/badge.svg?token=41784B23TS)](https://codecov.io/gh/thrasher-corp/gocryptotrader)
[![Go Report Card](https://goreportcard.com/badge/github.com/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***

View File

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

View File

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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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***

View File

@@ -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:

View File

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

View File

@@ -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:

View File

@@ -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:

View File

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

View File

@@ -41,8 +41,6 @@ type CoreSettings struct {
EnableGRPC bool
EnableGRPCProxy bool
EnableGRPCShutdown bool
EnableWebsocketRPC bool
EnableDeprecatedRPC bool
EnableCommsRelayer bool
EnableExchangeSyncManager bool
EnableDepositAddressManager bool

View File

@@ -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:

View File

@@ -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:

View File

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

View File

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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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:

View File

@@ -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: