diff --git a/.appveyor.yml b/.appveyor.yml index 892122be..fabc0659 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -26,7 +26,7 @@ environment: PSQL_SSLMODE: disable PSQL_SKIPSQLCMD: true PSQL_TESTDBNAME: gct_dev_ci -stack: go 1.15.x +stack: go 1.16.x services: - postgresql96 diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 72c84b30..a80cd383 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -10,6 +10,7 @@ vazha | https://github.com/vazha ermalguni | https://github.com/ermalguni MadCozBadd | https://github.com/MadCozBadd vadimzhukck | https://github.com/vadimzhukck +dependabot[bot] | https://github.com/apps/dependabot 140am | https://github.com/140am marcofranssen | https://github.com/marcofranssen dackroyd | https://github.com/dackroyd @@ -22,7 +23,6 @@ bretep | https://github.com/bretep Christian-Achilli | https://github.com/Christian-Achilli gam-phon | https://github.com/gam-phon cornelk | https://github.com/cornelk -dependabot[bot] | https://github.com/apps/dependabot if1live | https://github.com/if1live lozdog245 | https://github.com/lozdog245 soxipy | https://github.com/soxipy diff --git a/README.md b/README.md index 6f63278e..9f333fa3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + [![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) @@ -143,16 +143,17 @@ Binaries will be published once the codebase reaches a stable condition. |User|Contribution Amount| |--|--| -| [thrasher-](https://github.com/thrasher-) | 650 | -| [shazbert](https://github.com/shazbert) | 202 | -| [gloriousCode](https://github.com/gloriousCode) | 176 | -| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 87 | +| [thrasher-](https://github.com/thrasher-) | 654 | +| [shazbert](https://github.com/shazbert) | 207 | +| [gloriousCode](https://github.com/gloriousCode) | 179 | +| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 | | [xtda](https://github.com/xtda) | 47 | | [Rots](https://github.com/Rots) | 15 | | [vazha](https://github.com/vazha) | 15 | | [ermalguni](https://github.com/ermalguni) | 14 | -| [MadCozBadd](https://github.com/MadCozBadd) | 10 | +| [MadCozBadd](https://github.com/MadCozBadd) | 12 | | [vadimzhukck](https://github.com/vadimzhukck) | 10 | +| [dependabot[bot]](https://github.com/apps/dependabot) | 10 | | [140am](https://github.com/140am) | 8 | | [marcofranssen](https://github.com/marcofranssen) | 8 | | [dackroyd](https://github.com/dackroyd) | 5 | @@ -165,7 +166,6 @@ Binaries will be published once the codebase reaches a stable condition. | [Christian-Achilli](https://github.com/Christian-Achilli) | 2 | | [gam-phon](https://github.com/gam-phon) | 2 | | [cornelk](https://github.com/cornelk) | 2 | -| [dependabot[bot]](https://github.com/apps/dependabot) | 2 | | [if1live](https://github.com/if1live) | 2 | | [lozdog245](https://github.com/lozdog245) | 2 | | [soxipy](https://github.com/soxipy) | 2 | diff --git a/backtester/backtest/backtest.go b/backtester/backtest/backtest.go index fc6399fa..ae760ed2 100644 --- a/backtester/backtest/backtest.go +++ b/backtester/backtest/backtest.go @@ -40,6 +40,7 @@ import ( gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -281,7 +282,7 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange sellRule.Validate() limits, err := exch.GetOrderExecutionLimits(a, pair) - if err != nil { + if err != nil && !errors.Is(err, gctorder.ErrExchangeLimitNotLoaded) { return resp, err } @@ -361,15 +362,26 @@ func (bt *BackTest) setupBot(cfg *config.Config, bot *engine.Engine) error { if err != nil { return err } - + bt.Bot.ExchangeManager = engine.SetupExchangeManager() for i := range cfg.CurrencySettings { err = bt.Bot.LoadExchange(cfg.CurrencySettings[i].ExchangeName, false, nil) if err != nil && !errors.Is(err, engine.ErrExchangeAlreadyLoaded) { return err } } - if !bt.Bot.OrderManager.Started() { - return bt.Bot.OrderManager.Start(bt.Bot) + if !bt.Bot.OrderManager.IsRunning() { + bt.Bot.OrderManager, err = engine.SetupOrderManager( + bt.Bot.ExchangeManager, + bt.Bot.CommunicationsManager, + &bt.Bot.ServicesWG, + bot.Settings.Verbose) + if err != nil { + return err + } + err = bt.Bot.OrderManager.Start() + if err != nil { + return err + } } return nil @@ -470,18 +482,26 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange, if cfg.DataSettings.DatabaseData.ConfigOverride != nil { bt.Bot.Config.Database = *cfg.DataSettings.DatabaseData.ConfigOverride gctdatabase.DB.DataPath = filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "database") - gctdatabase.DB.Config = cfg.DataSettings.DatabaseData.ConfigOverride - err = bt.Bot.DatabaseManager.Start(bt.Bot) + err = gctdatabase.DB.SetConfig(cfg.DataSettings.DatabaseData.ConfigOverride) if err != nil { return nil, err } - defer func() { - err = bt.Bot.DatabaseManager.Stop() - if err != nil { - log.Error(log.BackTester, err) - } - }() } + bt.Bot.DatabaseManager, err = engine.SetupDatabaseConnectionManager(gctdatabase.DB.GetConfig()) + if err != nil { + return nil, err + } + + err = bt.Bot.DatabaseManager.Start(&bt.Bot.ServicesWG) + if err != nil { + return nil, err + } + defer func() { + stopErr := bt.Bot.DatabaseManager.Stop() + if stopErr != nil { + log.Error(log.BackTester, stopErr) + } + }() resp, err = loadDatabaseData(cfg, exch.GetName(), fPair, a, dataType) if err != nil { return nil, fmt.Errorf("unable to retrieve data from GoCryptoTrader database. Error: %v. Please ensure the database is setup correctly and has data before use", err) @@ -768,7 +788,7 @@ func (bt *BackTest) processDataEvent(e common.DataEventHandler) error { // updateStatsForDataEvent makes various systems aware of price movements from // data events func (bt *BackTest) updateStatsForDataEvent(e common.DataEventHandler) { - // update portfolio with latest price + // update portfoliomanager with latest price err := bt.Portfolio.Update(e) if err != nil { log.Error(log.BackTester, err) diff --git a/backtester/backtest/backtest_test.go b/backtester/backtest/backtest_test.go index c4b4ef5b..c5a0892f 100644 --- a/backtester/backtest/backtest_test.go +++ b/backtester/backtest/backtest_test.go @@ -30,7 +30,7 @@ import ( gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" ) -const testExchange = "binance" +const testExchange = "Bitstamp" func newBotWithExchange() (*engine.Engine, gctexchange.IBotExchange) { bot, err := engine.NewFromSettings(&engine.Settings{ @@ -40,6 +40,7 @@ func newBotWithExchange() (*engine.Engine, gctexchange.IBotExchange) { if err != nil { log.Fatal(err) } + bot.ExchangeManager = engine.SetupExchangeManager() err = bot.LoadExchange(testExchange, false, nil) if err != nil { log.Fatal(err) @@ -103,7 +104,7 @@ func TestNewFromConfig(t *testing.T) { } cfg.CurrencySettings[0].Base = "BTC" - cfg.CurrencySettings[0].Quote = "USDT" + cfg.CurrencySettings[0].Quote = "USD" cfg.DataSettings.APIData = &config.APIData{ StartDate: time.Time{}, @@ -162,7 +163,7 @@ func TestLoadData(t *testing.T) { Quote: "test", }, } - cfg.CurrencySettings[0].ExchangeName = testExchange + cfg.CurrencySettings[0].ExchangeName = "binance" cfg.CurrencySettings[0].Asset = asset.Spot.String() cfg.CurrencySettings[0].Base = "BTC" cfg.CurrencySettings[0].Quote = "USDT" @@ -193,7 +194,7 @@ func TestLoadData(t *testing.T) { Reports: &report.Data{}, } - cp := currency.NewPair(currency.BTC, currency.USDT) + cp := currency.NewPair(currency.BTC, currency.USD) _, err = bt.loadData(cfg, exch, cp, asset.Spot) if err != nil { t.Error(err) @@ -241,7 +242,7 @@ func TestLoadData(t *testing.T) { func TestLoadDatabaseData(t *testing.T) { t.Parallel() - cp := currency.NewPair(currency.BTC, currency.USDT) + cp := currency.NewPair(currency.BTC, currency.USD) _, err := loadDatabaseData(nil, "", cp, "", -1) if err != nil && !strings.Contains(err.Error(), "nil config data received") { t.Error(err) diff --git a/backtester/common/common_types.go b/backtester/common/common_types.go index 408c8892..2e497419 100644 --- a/backtester/common/common_types.go +++ b/backtester/common/common_types.go @@ -41,6 +41,8 @@ var ( ErrNilArguments = errors.New("received nil argument(s)") // ErrNilEvent is a common error for whenever a nil event occurs when it shouldn't have ErrNilEvent = errors.New("nil event received") + // ErrInvalidDataType occurs when an invalid data type is defined in the config + ErrInvalidDataType = errors.New("invalid datatype received") ) // EventHandler interface implements required GetTime() & Pair() return diff --git a/backtester/config/configbuilder/main.go b/backtester/config/configbuilder/main.go index 618ca84d..58ecea5b 100644 --- a/backtester/config/configbuilder/main.go +++ b/backtester/config/configbuilder/main.go @@ -395,7 +395,10 @@ func parseDatabase(reader *bufio.Reader, cfg *config.Config) error { } } cfg.DataSettings.DatabaseData.ConfigOverride.Port = uint16(port) - database.DB.Config = cfg.DataSettings.DatabaseData.ConfigOverride + err = database.DB.SetConfig(cfg.DataSettings.DatabaseData.ConfigOverride) + if err != nil { + return fmt.Errorf("database failed to set config: %w", err) + } if cfg.DataSettings.DatabaseData.ConfigOverride.Driver == database.DBPostgreSQL { _, err = dbPSQL.Connect() if err != nil { diff --git a/backtester/data/kline/api/api.go b/backtester/data/kline/api/api.go index 5b34e45b..737fa25c 100644 --- a/backtester/data/kline/api/api.go +++ b/backtester/data/kline/api/api.go @@ -44,7 +44,7 @@ func LoadData(dataType int64, startDate, endDate time.Time, interval time.Durati return nil, fmt.Errorf("could not convert trade data to candles for %v %v %v, %v", exch.GetName(), a, fPair, err) } default: - return nil, fmt.Errorf("could not retrieve data for %v %v %v, invalid data type received", exch.GetName(), a, fPair) + return nil, fmt.Errorf("could not retrieve data for %v %v %v, %w", exch.GetName(), a, fPair, common.ErrInvalidDataType) } candles.Exchange = strings.ToLower(candles.Exchange) diff --git a/backtester/data/kline/api/api_test.go b/backtester/data/kline/api/api_test.go index 12e8c163..a6cd5528 100644 --- a/backtester/data/kline/api/api_test.go +++ b/backtester/data/kline/api/api_test.go @@ -1,10 +1,10 @@ package api import ( + "errors" "log" "os" "path/filepath" - "strings" "testing" "time" @@ -33,6 +33,7 @@ func TestMain(m *testing.M) { log.Fatal(err) } + bot.ExchangeManager = engine.SetupExchangeManager() err = bot.LoadExchange(testExchange, false, nil) if err != nil { log.Fatal(err) @@ -62,15 +63,15 @@ func TestLoadCandles(t *testing.T) { } _, err = LoadData(-1, tt1, tt2, interval.Duration(), exch, p, a) - if err != nil && !strings.Contains(err.Error(), "could not retrieve data for Binance spot BTCUSDT, invalid data type received") { - t.Error(err) + if !errors.Is(err, common.ErrInvalidDataType) { + t.Errorf("expected '%v' received '%v'", err, common.ErrInvalidDataType) } } func TestLoadTrades(t *testing.T) { t.Parallel() interval := gctkline.FifteenMin - tt1 := time.Now().Add(-time.Minute * 60).Round(interval.Duration()) + tt1 := time.Now().Add(-time.Minute * 15).Round(interval.Duration()) tt2 := time.Now().Round(interval.Duration()) a := asset.Spot p := currency.NewPair(currency.BTC, currency.USDT) diff --git a/backtester/data/kline/csv/csv.go b/backtester/data/kline/csv/csv.go index 76187c1a..7935559f 100644 --- a/backtester/data/kline/csv/csv.go +++ b/backtester/data/kline/csv/csv.go @@ -149,7 +149,7 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat return nil, fmt.Errorf("could not read csv trade data for %v %v %v, %v", exchangeName, a, fPair, err) } default: - return nil, fmt.Errorf("could not process csv data for %v %v %v, invalid data type received", exchangeName, a, fPair) + return nil, fmt.Errorf("could not process csv data for %v %v %v, %w", exchangeName, a, fPair, common.ErrInvalidDataType) } resp.Item.Exchange = strings.ToLower(exchangeName) resp.Item.Pair = fPair diff --git a/backtester/data/kline/csv/csv_test.go b/backtester/data/kline/csv/csv_test.go index 2d0e56d6..02b2728a 100644 --- a/backtester/data/kline/csv/csv_test.go +++ b/backtester/data/kline/csv/csv_test.go @@ -1,8 +1,8 @@ package csv import ( + "errors" "path/filepath" - "strings" "testing" "github.com/thrasher-corp/gocryptotrader/backtester/common" @@ -56,7 +56,7 @@ func TestLoadDataInvalid(t *testing.T) { gctkline.FifteenMin.Duration(), p, a) - if err != nil && !strings.Contains(err.Error(), "could not process csv data for binance spot BTCUSDT, invalid data type received") { - t.Error(err) + if !errors.Is(err, common.ErrInvalidDataType) { + t.Errorf("expected '%v' received '%v'", err, common.ErrInvalidDataType) } } diff --git a/backtester/data/kline/database/database.go b/backtester/data/kline/database/database.go index 5dd1cdc7..98cf0b2d 100644 --- a/backtester/data/kline/database/database.go +++ b/backtester/data/kline/database/database.go @@ -48,7 +48,7 @@ func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName } resp.Item = klineItem default: - return nil, fmt.Errorf("could not retrieve database data for %v %v %v, invalid data type received", exchangeName, a, fPair) + return nil, fmt.Errorf("could not retrieve database data for %v %v %v, %w", exchangeName, a, fPair, common.ErrInvalidDataType) } resp.Item.Exchange = strings.ToLower(resp.Item.Exchange) diff --git a/backtester/data/kline/database/database_test.go b/backtester/data/kline/database/database_test.go index 8dfd414e..85f2732e 100644 --- a/backtester/data/kline/database/database_test.go +++ b/backtester/data/kline/database/database_test.go @@ -1,11 +1,11 @@ package database import ( + "errors" "fmt" "io/ioutil" "os" "path/filepath" - "strings" "testing" "time" @@ -86,7 +86,11 @@ func TestLoadDataCandles(t *testing.T) { t.Error(err) } - err = bot.DatabaseManager.Start(bot) + bot.DatabaseManager, err = engine.SetupDatabaseConnectionManager(&bot.Config.Database) + if err != nil { + t.Error(err) + } + err = bot.DatabaseManager.Start(&bot.ServicesWG) if err != nil { t.Error(err) } @@ -161,7 +165,11 @@ func TestLoadDataTrades(t *testing.T) { t.Error(err) } - err = bot.DatabaseManager.Start(bot) + bot.DatabaseManager, err = engine.SetupDatabaseConnectionManager(&bot.Config.Database) + if err != nil { + t.Error(err) + } + err = bot.DatabaseManager.Start(&bot.ServicesWG) if err != nil { t.Error(err) } @@ -202,7 +210,7 @@ func TestLoadDataInvalid(t *testing.T) { dStart := time.Date(2020, 1, 0, 0, 0, 0, 0, time.UTC) dEnd := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) _, err := LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, -1, p, a) - if err != nil && !strings.Contains(err.Error(), "could not retrieve database data for binance spot BTCUSDT, invalid data type received") { - t.Error(err) + if !errors.Is(err, common.ErrInvalidDataType) { + t.Errorf("expected '%v' received '%v'", err, common.ErrInvalidDataType) } } diff --git a/backtester/data/kline/live/live.go b/backtester/data/kline/live/live.go index 9d4ea4cc..bda490d5 100644 --- a/backtester/data/kline/live/live.go +++ b/backtester/data/kline/live/live.go @@ -58,7 +58,7 @@ func LoadData(exch exchange.IBotExchange, dataType int64, interval time.Duration } } default: - return nil, fmt.Errorf("could not retrieve live data for %v %v %v, invalid data type received", exch.GetName(), a, fPair) + return nil, fmt.Errorf("could not retrieve live data for %v %v %v, %w", exch.GetName(), a, fPair, common.ErrInvalidDataType) } candles.Exchange = strings.ToLower(exch.GetName()) return &candles, nil diff --git a/backtester/data/kline/live/live_test.go b/backtester/data/kline/live/live_test.go index 7f75d572..d87b71ec 100644 --- a/backtester/data/kline/live/live_test.go +++ b/backtester/data/kline/live/live_test.go @@ -1,40 +1,38 @@ package live import ( + "errors" + "log" "path/filepath" - "strings" "testing" "github.com/thrasher-corp/gocryptotrader/backtester/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" ) -const testExchange = "binance" +const testExchange = "FTX" func TestLoadCandles(t *testing.T) { t.Parallel() interval := gctkline.FifteenMin - bot, err := engine.NewFromSettings(&engine.Settings{ - ConfigFile: filepath.Join("..", "..", "..", "..", "testdata", "configtest.json"), - EnableDryRun: true, - }, nil) + bot := new(engine.Engine) + bot.Config = &config.Config{} + err := bot.Config.LoadConfig(filepath.Join("..", "..", "..", "..", "testdata", "configtest.json"), true) if err != nil { - t.Error(err) + t.Fatalf("SetupTest: Failed to load config: %s", err) } - + bot.ExchangeManager = engine.SetupExchangeManager() err = bot.LoadExchange(testExchange, false, nil) if err != nil { - t.Fatal(err) - } - exch := bot.GetExchangeByName(testExchange) - if exch == nil { - t.Fatal("expected binance") + log.Fatal(err) } + exch := bot.ExchangeManager.GetExchangeByName(testExchange) a := asset.Spot - p := currency.NewPair(currency.BTC, currency.USDT) + p := currency.NewPair(currency.BTC, currency.USD) var data *gctkline.Item data, err = LoadData(exch, common.DataCandle, interval.Duration(), p, a) if err != nil { @@ -45,8 +43,8 @@ func TestLoadCandles(t *testing.T) { } _, err = LoadData(exch, -1, interval.Duration(), p, a) - if err != nil && !strings.Contains(err.Error(), "could not retrieve live data for Binance spot BTCUSDT, invalid data type received") { - t.Error(err) + if !errors.Is(err, common.ErrInvalidDataType) { + t.Errorf("expected '%v' received '%v'", err, common.ErrInvalidDataType) } } @@ -60,6 +58,7 @@ func TestLoadTrades(t *testing.T) { if err != nil { t.Error(err) } + bot.ExchangeManager = engine.SetupExchangeManager() err = bot.LoadExchange(testExchange, false, nil) if err != nil { diff --git a/backtester/eventhandlers/exchange/exchange_test.go b/backtester/eventhandlers/exchange/exchange_test.go index f2d26b4d..01938832 100644 --- a/backtester/eventhandlers/exchange/exchange_test.go +++ b/backtester/eventhandlers/exchange/exchange_test.go @@ -139,7 +139,13 @@ func TestPlaceOrder(t *testing.T) { if err != nil { t.Fatal(err) } - err = bot.OrderManager.Start(bot) + em := engine.SetupExchangeManager() + bot.ExchangeManager = em + bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false) + if err != nil { + t.Error(err) + } + err = bot.OrderManager.Start() if err != nil { t.Error(err) } @@ -187,7 +193,13 @@ func TestExecuteOrder(t *testing.T) { t.Fatal(err) } - err = bot.OrderManager.Start(bot) + em := engine.SetupExchangeManager() + bot.ExchangeManager = em + bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false) + if err != nil { + t.Error(err) + } + err = bot.OrderManager.Start() if err != nil { t.Error(err) } @@ -287,8 +299,14 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) { if err != nil { t.Fatal(err) } + em := engine.SetupExchangeManager() + bot.ExchangeManager = em + bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false) + if err != nil { + t.Error(err) + } - err = bot.OrderManager.Start(bot) + err = bot.OrderManager.Start() if err != nil { t.Error(err) } diff --git a/cmd/apichecker/README.md b/cmd/apichecker/README.md index 66d9ef97..620045ca 100644 --- a/cmd/apichecker/README.md +++ b/cmd/apichecker/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Apichecker - + [![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) diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go index 7542cc09..788315bb 100644 --- a/cmd/config/config_test.go +++ b/cmd/config/config_test.go @@ -6,13 +6,13 @@ func TestEncryptOrDecrypt(t *testing.T) { reValue := EncryptOrDecrypt(true) if reValue != "encrypted" { t.Error( - "Tools/Config/Config_test.go - EncryptOrDecrypt Error", + "expected encrypted", ) } reValue = EncryptOrDecrypt(false) if reValue != "decrypted" { t.Error( - "Tools/Config/Config_test.go - EncryptOrDecrypt Error", + "expected decrypted", ) } } diff --git a/cmd/documentation/README.md b/cmd/documentation/README.md index 99babe32..13beb180 100644 --- a/cmd/documentation/README.md +++ b/cmd/documentation/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Documentation - + [![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) diff --git a/cmd/documentation/documentation.go b/cmd/documentation/documentation.go index 8ba4c610..fa0facc3 100644 --- a/cmd/documentation/documentation.go +++ b/cmd/documentation/documentation.go @@ -63,7 +63,8 @@ var ( repoDir string // is a broken down version of the documentation tool dir for cross platform // checking - ref = []string{"gocryptotrader", "cmd", "documentation"} + ref = []string{"gocryptotrader", "cmd", "documentation"} + engineFolder = "engine" ) // Contributor defines an account associated with this code base by doing @@ -121,8 +122,8 @@ func main() { } if strings.Contains(wd, filepath.Join(ref...)) { - rootdir := filepath.Dir(filepath.Dir(wd)) - repoDir = rootdir + rootDir := filepath.Dir(filepath.Dir(wd)) + repoDir = rootDir toolDir = wd } else { if toolDir == "" { @@ -132,7 +133,7 @@ func main() { repoDir = wd } - fmt.Println(core.Banner) + fmt.Print(core.Banner) fmt.Println("This will update and regenerate documentation for the different packages in your repo.") fmt.Println() @@ -272,15 +273,11 @@ func main() { fmt.Println("All core systems fetched, updating documentation...") } - err = UpdateDocumentation(DocumentationDetails{ + UpdateDocumentation(DocumentationDetails{ dirList, tmpl, contributors, &config}) - if err != nil { - log.Fatalf("Documentation Generation Tool - UpdateDocumentation error %s", - err) - } fmt.Println("\nDocumentation Generation Tool - Finished") } @@ -355,7 +352,7 @@ func GetProjectDirectoryTree(c *Config) ([]string, error) { directoryData = append(directoryData, filepath.Join(repoDir, ContributorFile)) } - walkfn := func(path string, info os.FileInfo, err error) error { + walkFn := func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -376,7 +373,7 @@ func GetProjectDirectoryTree(c *Config) ([]string, error) { return nil } - return directoryData, filepath.Walk(repoDir, walkfn) + return directoryData, filepath.Walk(repoDir, walkFn) } // GetTemplateFiles parses and returns all template files in the documentation @@ -384,7 +381,7 @@ func GetProjectDirectoryTree(c *Config) ([]string, error) { func GetTemplateFiles() (*template.Template, error) { tmpl := template.New("") - walkfn := func(path string, info os.FileInfo, err error) error { + walkFn := func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -406,7 +403,7 @@ func GetTemplateFiles() (*template.Template, error) { return nil } - return tmpl, filepath.Walk(toolDir, walkfn) + return tmpl, filepath.Walk(toolDir, walkFn) } // GetContributorList fetches a list of contributors from the github api @@ -458,14 +455,14 @@ func GetGoDocURL(name string) string { // UpdateDocumentation generates or updates readme/documentation files across // the codebase -func UpdateDocumentation(details DocumentationDetails) error { +func UpdateDocumentation(details DocumentationDetails) { for i := range details.Directories { - cutset := details.Directories[i][len(repoDir):] - if cutset != "" && cutset[0] == os.PathSeparator { - cutset = cutset[1:] + cutSet := details.Directories[i][len(repoDir):] + if cutSet != "" && cutSet[0] == os.PathSeparator { + cutSet = cutSet[1:] } - data := strings.Split(cutset, string(os.PathSeparator)) + data := strings.Split(cutSet, string(os.PathSeparator)) var temp []string for x := range data { @@ -491,40 +488,66 @@ func UpdateDocumentation(details DocumentationDetails) error { } continue } - + if name == engineFolder { + d, err := ioutil.ReadDir(details.Directories[i]) + if err != nil { + fmt.Println("Excluding file:", err) + } + for x := range d { + nameSplit := strings.Split(d[x].Name(), ".go") + engineTemplateName := engineFolder + " " + nameSplit[0] + if details.Tmpl.Lookup(engineTemplateName) == nil { + fmt.Printf("Template not found for path %s create new template with {{define \"%s\" -}} TEMPLATE HERE {{end}}\n", + details.Directories[i], + name) + continue + } + err = runTemplate(details, filepath.Join(details.Directories[i], nameSplit[0]+".md"), engineTemplateName) + if err != nil { + fmt.Println(err) + } + } + continue + } if details.Tmpl.Lookup(name) == nil { fmt.Printf("Template not found for path %s create new template with {{define \"%s\" -}} TEMPLATE HERE {{end}}\n", details.Directories[i], name) continue } - var mainPath string - if name == LicenseFile || name == ContributorFile { + switch { + case name == LicenseFile || name == ContributorFile: mainPath = details.Directories[i] - } else { + default: mainPath = filepath.Join(details.Directories[i], "README.md") } - err := os.Remove(mainPath) - if err != nil && !(strings.Contains(err.Error(), "no such file or directory") || - strings.Contains(err.Error(), "The system cannot find the file specified.")) { - return err + if err := runTemplate(details, mainPath, name); err != nil { + log.Println(err) + continue } - - file, err := os.Create(mainPath) - if err != nil { - return err - } - - attr := GetDocumentationAttributes(name, details.Contributors) - - err = details.Tmpl.ExecuteTemplate(file, name, attr) - if err != nil { - file.Close() - return err - } - file.Close() } - return nil +} + +func runTemplate(details DocumentationDetails, mainPath, name string) error { + err := os.Remove(mainPath) + if err != nil && !(strings.Contains(err.Error(), "no such file or directory") || + strings.Contains(err.Error(), "The system cannot find the file specified.")) { + return err + } + + f, err := os.Create(mainPath) + if err != nil { + return err + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + log.Printf("could not close file %s: %v", mainPath, err) + } + }(f) + + attr := GetDocumentationAttributes(name, details.Contributors) + return details.Tmpl.ExecuteTemplate(f, name, attr) } diff --git a/cmd/documentation/engine_templates/apiserver.tmpl b/cmd/documentation/engine_templates/apiserver.tmpl new file mode 100644 index 00000000..5ffddb71 --- /dev/null +++ b/cmd/documentation/engine_templates/apiserver.tmpl @@ -0,0 +1,28 @@ +{{define "engine apiserver" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ 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` | + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/communication_manager.tmpl b/cmd/documentation/engine_templates/communication_manager.tmpl new file mode 100644 index 00000000..5f1e3cba --- /dev/null +++ b/cmd/documentation/engine_templates/communication_manager.tmpl @@ -0,0 +1,56 @@ +{{define "engine communication_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The communication manager subsystem is used to push events raised in GoCryptoTrader to any enabled communication system such as a Slack server ++ In order to modify the behaviour of the communication manager subsystem, you can edit the following inside your config file under `communications`: + +### slack + +| Config | Description | Example | +| ------ | ----------- | ------- | +| enabled | Determines whether the push communications to a Slack server | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| targetChannel | The channel to send communications to | `announcements` | +| verificationToken | The token generated by Slack to allow interactions with the server and channel | `iamafaketoken` | + +### smsGlobal + +| Config | Description | Example | +| ------ | ----------- | ------- | +| name | The name of the SMS sender | `SMSGlobal` | +| from | Who the text name is from | `Skynet` | +| enabled | Determines whether the push communications to the SMS service | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| username | The username to use with the SMS provider | `username` | +| password | The username to use with the SMS provider | `password` | +| contacts | The `name` `number` of the user people you wish to send SMS to and whether it is `enabled` | `"name": "StyleGherkin", "number": "1231424", "enabled": true` | + +### smtp + +| Config | Description | Example | +| ------ | ----------- | ------- | +| name | The name of the service | `SMTP` | +| enabled | Determines whether the push communications to a email server | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| host | The SMTP host | `smtp.google.com` | +| port | The port to use | `537` | +| accountName | Your username | `username` | +| accountPassword | Your password | `password` | +| from | The display name of the sender | `Jeff Bezos` | +| recipientList | A comma delimited list of addresses to send alerts to | `bill@gates.com` | + +### telegram + +| Config | Description | Example | +| ------ | ----------- | ------- | +| name | The name to be displayed | `Telegram` | +| enabled | Determines whether the push communications to a Telegram server | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| verificationToken | The token generated by Telegram to allow you to send messages | `iamafaketoken` | + + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/connection_manager.tmpl b/cmd/documentation/engine_templates/connection_manager.tmpl new file mode 100644 index 00000000..8d66e8ad --- /dev/null +++ b/cmd/documentation/engine_templates/connection_manager.tmpl @@ -0,0 +1,19 @@ +{{define "engine connection_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The connection manager subsystem is used to periodically check whether the application is connected to the internet and will provide alerts of any changes ++ In order to modify the behaviour of the connection manager subsystem, you can edit the following inside your config file under `connectionMonitor`: + +### connectionMonitor + +| Config | Description | Example | +| ------ | ----------- | ------- | +| perferredDNSList | Is a string array of DNS servers to periodically verify whether GoCryptoTrader is connected to the internet | `["8.8.8.8","8.8.4.4","1.1.1.1","1.0.0.1"]` | +| preferredDomainList | Is a string array of domains to periodically verify whether GoCryptoTrader is connected to the internet | `["www.google.com","www.cloudflare.com","www.facebook.com"]` | +| checkInterval | A time period in golang `time.Duration` format to check whether GoCryptoTrader is connected to the internet | `1000000000` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/database_connection.tmpl b/cmd/documentation/engine_templates/database_connection.tmpl new file mode 100644 index 00000000..e2f9dd84 --- /dev/null +++ b/cmd/documentation/engine_templates/database_connection.tmpl @@ -0,0 +1,30 @@ +{{define "engine database_connection" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The database connection manager subsystem is used to periodically check whether the application is connected to the database and will provide alerts of any changes ++ In order to modify the behaviour of the database connection manager subsystem, you can edit the following inside your config file under `database`: + +### database + +| Config | Description | Example | +| ------ | ----------- | ------- | +| enabled | Enabled or disables the database connection subsystem | `true` | +| verbose | Displays more information to the logger which can be helpful for debugging | `false` | +| driver | The SQL driver to use. Can be `postgres` or `sqlite`. | `sqlite` | +| connectionDetails | See below | | + +### connectionDetails + +| Config | Description | Example | +| ------ | ----------- | ------- | +| host | The host address of the database | `localhost` | +| port | The port used to connect to the database | `5432` | +| username | An optional username to connect to the database | `username` | +| password | An optional password to connect to the database | `password` | +| database | The name of the database | `database.db` | +| sslmode | The connection type of the database for Postgres databases only | `disable` | + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/depositaddress.tmpl b/cmd/documentation/engine_templates/depositaddress.tmpl new file mode 100644 index 00000000..300600be --- /dev/null +++ b/cmd/documentation/engine_templates/depositaddress.tmpl @@ -0,0 +1,11 @@ +{{define "engine depositaddress" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The deposit address manager subsystem stores Exchange deposit addresses. ++ On start of the application the engine Bot will retrieve deposit addresses from exchanges if you have API keys set + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/event_manager.tmpl b/cmd/documentation/engine_templates/event_manager.tmpl new file mode 100644 index 00000000..95ad59e2 --- /dev/null +++ b/cmd/documentation/engine_templates/event_manager.tmpl @@ -0,0 +1,18 @@ +{{define "engine event_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The event manager subsystem is used to push events to communication systems such as Slack ++ The only configurable aspects of the event manager are the delays between receiving an event and pushing it and enabling verbose: + +### connectionMonitor + +| Config | Description | Example | +| ------ | ----------- | ------- | +| eventmanagerdelay | Sets the event managers sleep delay between event checking by a Golang `time.Duration` | `0` | +| verbose | Outputs debug messaging allowing for greater transparency for what the event manager is doing | `false` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/exchange_manager.tmpl b/cmd/documentation/engine_templates/exchange_manager.tmpl new file mode 100644 index 00000000..86ddce10 --- /dev/null +++ b/cmd/documentation/engine_templates/exchange_manager.tmpl @@ -0,0 +1,11 @@ +{{define "engine exchange_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The exchange manager subsystem is used load and store exchanges so that the engine Bot can use them to track orderbooks, submit orders etc etc ++ The exchange manager itself is not customisable, it is always enabled. ++ The exchange manager by default will load all exchanges that are enabled in your config, however, it will also load exchanges by request via GRPC commands + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/ntp_manager.tmpl b/cmd/documentation/engine_templates/ntp_manager.tmpl new file mode 100644 index 00000000..e1e6c252 --- /dev/null +++ b/cmd/documentation/engine_templates/ntp_manager.tmpl @@ -0,0 +1,22 @@ +{{define "engine ntp_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The NTP manager subsystem is used highlight discrepancies between your system time and specified NTP server times ++ It is useful for debugging and understanding why a request to an exchange may be rejected ++ The NTP manager cannot update your system clock, so when it does alert you of issues, you must take it upon yourself to change your system time in the event your requests are being rejected for being too far out of sync ++ In order to modify the behaviour of the NTP manager subsystem, you can edit the following inside your config file under `ntpclient`: + +### ntpclient + +| Config | Description | Example | +| ------ | ----------- | ------- | +| enabled | An integer value representing whether the NTP manager is enabled. It will warn you of time sync discrepancies on startup with a value of 0 and will alert you periodically with a value of 1. A value of -1 will disable the manager | `1` | +| pool | A string array of the NTP servers to check for time discrepancies | `["0.pool.ntp.org:123","pool.ntp.org:123"]` | +| allowedDifference | A Golang time.Duration representation of the allowable time discrepancy between NTP server and your system time. Any discrepancy greater than this allowance will display an alert to your logging output | `50000000` | +| allowedNegativeDifference | A Golang time.Duration representation of the allowable negative time discrepancy between NTP server and your system time. Any discrepancy greater than this allowance will display an alert to your logging output | `50000000` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/order_manager.tmpl b/cmd/documentation/engine_templates/order_manager.tmpl new file mode 100644 index 00000000..a7b0e08e --- /dev/null +++ b/cmd/documentation/engine_templates/order_manager.tmpl @@ -0,0 +1,11 @@ +{{define "engine order_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The order manager subsystem stores and monitors all orders from enabled exchanges with API keys and `authenticatedSupport` enabled ++ It can be enabled or disabled via runtime command `-ordermanager=false` and defaults to true ++ All orders placed via GoCryptoTrader will be added to the order manager store + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/portfolio_manager.tmpl b/cmd/documentation/engine_templates/portfolio_manager.tmpl new file mode 100644 index 00000000..ff7fcf8b --- /dev/null +++ b/cmd/documentation/engine_templates/portfolio_manager.tmpl @@ -0,0 +1,32 @@ +{{define "engine portfolio_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The portfolio manager subsystem is used to synchronise and monitor wallet addresses ++ It can read addresses specified in your config file ++ If you have set API keys for an enabled exchange and enabled `authenticatedSupport`, it will store your exchange addresses ++ In order to modify the behaviour of the portfolio manager subsystem, you can edit the following inside your config file under `portfolioAddresses`: + +### portfolioAddresses + +| Config | Description | Example | +| ------ | ----------- | ------- | +| Verbose | Enabling this will output more detailed logs to your logging output | `false` | +| addresses | An array of portfolio wallet addresses to monitor, see below table | | + +### addresses + +| Config | Description | Example | +| ------ | ----------- | ------- | +| Address | The wallet address | `{{.DonationAddress}}` | +| CoinType | The coin for the wallet address | `BTC` | +| Balance | The balance of the wallet | | +| Description | A customisable description | `My secret billion stash` | +| WhiteListed | Determines whether GoCryptoTrader withdraw manager subsystem can make withdrawals from this address | `true` | +| ColdStorage | Describes whether the wallet address is a cold storage wallet eg Ledger | `false` | +| SupportedExchanges | A comma delimited string of which exchanges are allowed to interact with this wallet | `"Binance"` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/subsystem_types.tmpl b/cmd/documentation/engine_templates/subsystem_types.tmpl new file mode 100644 index 00000000..87551e2f --- /dev/null +++ b/cmd/documentation/engine_templates/subsystem_types.tmpl @@ -0,0 +1,13 @@ +{{define "engine subsystem_types" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ Subsystem contains subsystems that are used at run time by an `engine.Engine`, however they can be setup and run individually. ++ Subsystems are designed to be self contained ++ All subsystems have a public `Setup(...) (..., error)` function to return a valid subsystem ready for use + + Subsystems which are designed to be switched off also have `Start(...) error`, `IsRunning() bool` and `Stop(...) error` functions to allow the main `engine.Engine` instance to manage them ++ Common subsystem types such as errors can be found within the `subsystem.go` file + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/sync_manager.tmpl b/cmd/documentation/engine_templates/sync_manager.tmpl new file mode 100644 index 00000000..f6910e06 --- /dev/null +++ b/cmd/documentation/engine_templates/sync_manager.tmpl @@ -0,0 +1,22 @@ +{{define "engine sync_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The currency pair syncer subsystem is used to keep all trades, tickers and orderbooks up to date for all enabled exchange asset currency pairs ++ It can sync data via a websocket connection or REST and will switch between them if there has been no updates ++ In order to modify the behaviour of the currency pair syncer subsystem, you can change runtime parameters as detailed below: + +| Config | Description | Example | +| ------ | ----------- | ------- | +| syncmanager | Determines whether the subsystem is enabled | `true` | +| tickersync | Enables ticker syncing for all enabled exchanges | `true`| +| orderbooksync | Enables orderbook syncing for all enabled exchanges | `true` | +| tradesync | Enables trade syncing for all enabled exchanges | `true` | +| syncworkers | The amount of workers (goroutines) to use for syncing exchange data | `15` | +| synccontinuously | Whether to sync exchange data continuously (ticker, orderbook and trades) | `true` | +| synctimeout | The amount of time in golang `time.Duration` format before the syncer will switch from one protocol to the other (e.g. from REST to websocket) | `15000000000` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/websocketroutine_manager.tmpl b/cmd/documentation/engine_templates/websocketroutine_manager.tmpl new file mode 100644 index 00000000..77bcbf5e --- /dev/null +++ b/cmd/documentation/engine_templates/websocketroutine_manager.tmpl @@ -0,0 +1,14 @@ +{{define "engine websocketroutine_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The websocket routine manager subsystem is used process websocket data in a unified manner across enabled exchanges with websocket support ++ It can help process orders to the order manager subsystem when it receives new data ++ Logs output of ticker and orderbook updates ++ The websocket routine manager subsystem can be enabled or disabled via runtime command `-websocketroutine=false` defaulting to true ++ Logs can be customised to display values the config value `fiatDisplayCurrency` under `currencyConfig` + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/engine_templates/withdraw_manager.tmpl b/cmd/documentation/engine_templates/withdraw_manager.tmpl new file mode 100644 index 00000000..88ccdaff --- /dev/null +++ b/cmd/documentation/engine_templates/withdraw_manager.tmpl @@ -0,0 +1,15 @@ +{{define "engine withdraw_manager" -}} +{{template "header" .}} +## Current Features for {{.CapitalName}} ++ The withdraw manager subsystem is responsible for the processing of withdrawal requests and submitting them to exchanges ++ The withdraw manager can be interacted with via GRPC commands such as `WithdrawFiatRequest` and `WithdrawCryptoRequest` ++ Supports caching of responses to allow for quick viewing of withdrawal events via GRPC ++ If the database is enabled, withdrawal events are stored to the database for later viewing ++ Will not process withdrawal events if `dryrun` is true ++ The withdraw manager subsystem is always enabled + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/root_templates/root_readme.tmpl b/cmd/documentation/root_templates/root_readme.tmpl index 31a236c1..2d1625d1 100644 --- a/cmd/documentation/root_templates/root_readme.tmpl +++ b/cmd/documentation/root_templates/root_readme.tmpl @@ -1,5 +1,5 @@ {{define "root" -}} - + [![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) diff --git a/cmd/documentation/sub_templates/header.tmpl b/cmd/documentation/sub_templates/header.tmpl index 36284bea..82c8ad8e 100644 --- a/cmd/documentation/sub_templates/header.tmpl +++ b/cmd/documentation/sub_templates/header.tmpl @@ -1,7 +1,7 @@ {{define "header" -}} # GoCryptoTrader package {{.CapitalName}} - + {{template "status" .NameURL}} diff --git a/cmd/exchange_template/readme_file.tmpl b/cmd/exchange_template/readme_file.tmpl index 8a096a02..e2a2ea5a 100644 --- a/cmd/exchange_template/readme_file.tmpl +++ b/cmd/exchange_template/readme_file.tmpl @@ -1,7 +1,7 @@ {{- define "readme"}} # GoCryptoTrader {{.CapitalName}} Exchange Wrapper - + An exchange interface wrapper for the GoCryptoTrader application. diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index d9551e83..4979d558 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -730,13 +730,17 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }, Amount: config.OrderSubmission.Amount, } - var withdrawCryptocurrencyFundsResponse *withdraw.ExchangeResponse - withdrawCryptocurrencyFundsResponse, err = e.WithdrawCryptocurrencyFunds(&withdrawRequest) msg = "" + err = withdrawRequest.Validate() if err != nil { msg = err.Error() responseContainer.ErrorCount++ } + withdrawCryptocurrencyFundsResponse, err := e.WithdrawCryptocurrencyFunds(&withdrawRequest) + if err != nil { + msg += ", " + err.Error() + responseContainer.ErrorCount++ + } responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ SentParams: jsonifyInterface([]interface{}{withdrawRequest}), Function: "WithdrawCryptocurrencyFunds", @@ -796,8 +800,7 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) IntermediaryBankCode: config.BankDetails.IntermediaryBankCode, }, } - var withdrawFiatFundsResponse *withdraw.ExchangeResponse - withdrawFiatFundsResponse, err = e.WithdrawFiatFunds(&withdrawRequestFiat) + withdrawFiatFundsResponse, err := e.WithdrawFiatFunds(&withdrawRequestFiat) msg = "" if err != nil { msg = err.Error() @@ -810,8 +813,7 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) Response: withdrawFiatFundsResponse, }) - var withdrawFiatFundsInternationalResponse *withdraw.ExchangeResponse - withdrawFiatFundsInternationalResponse, err = e.WithdrawFiatFundsToInternationalBank(&withdrawRequestFiat) + withdrawFiatFundsInternationalResponse, err := e.WithdrawFiatFundsToInternationalBank(&withdrawRequestFiat) msg = "" if err != nil { msg = err.Error() diff --git a/cmd/gctcli/commands.go b/cmd/gctcli/commands.go index 8a6f4108..c906e754 100644 --- a/cmd/gctcli/commands.go +++ b/cmd/gctcli/commands.go @@ -2225,11 +2225,11 @@ var addEventCommand = cli.Command{ }, cli.BoolFlag{ Name: "check_bids", - Usage: "whether to check the bids (if false, asks will be used)", + Usage: "whether to check the bids", }, cli.BoolFlag{ - Name: "check_bids_and_asks", - Usage: "the wallet address", + Name: "check_asks", + Usage: "whether to check the asks", }, cli.Float64Flag{ Name: "orderbook_amount", @@ -2260,7 +2260,7 @@ func addEvent(c *cli.Context) error { var condition string var price float64 var checkBids bool - var checkBidsAndAsks bool + var checkAsks bool var orderbookAmount float64 var currencyPair string var assetType string @@ -2296,8 +2296,8 @@ func addEvent(c *cli.Context) error { checkBids = c.Bool("check_bids") } - if c.IsSet("check_bids_and_asks") { - checkBids = c.Bool("check_bids_and_asks") + if c.IsSet("check_asks") { + checkAsks = c.Bool("check_asks") } if c.IsSet("orderbook_amount") { @@ -2345,11 +2345,11 @@ func addEvent(c *cli.Context) error { Exchange: exchangeName, Item: item, ConditionParams: &gctrpc.ConditionParams{ - Condition: condition, - Price: price, - CheckBids: checkBids, - CheckBidsAndAsks: checkBidsAndAsks, - OrderbookAmount: orderbookAmount, + Condition: condition, + Price: price, + CheckBids: checkBids, + CheckAsks: checkAsks, + OrderbookAmount: orderbookAmount, }, Pair: &gctrpc.CurrencyPair{ Delimiter: p.Delimiter, diff --git a/cmd/gen_sqlboiler_config/main.go b/cmd/gen_sqlboiler_config/main.go index 2409e81f..ac62e31f 100644 --- a/cmd/gen_sqlboiler_config/main.go +++ b/cmd/gen_sqlboiler_config/main.go @@ -93,5 +93,6 @@ func convertGCTtoSQLBoilerConfig(c *database.Config) { // getLoadedDBPath gets the path loaded by 'database/drivers/sqlite3' func getLoadedDBPath() string { - return filepath.Join(database.DB.DataPath, database.DB.Config.Database) + cfg := database.DB.GetConfig() + return filepath.Join(database.DB.DataPath, cfg.Database) } diff --git a/common/README.md b/common/README.md index e27c0878..4a775b9b 100644 --- a/common/README.md +++ b/common/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Common - + [![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) diff --git a/common/common.go b/common/common.go index 7551a9ef..85b10ead 100644 --- a/common/common.go +++ b/common/common.go @@ -15,6 +15,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/thrasher-corp/gocryptotrader/log" @@ -32,6 +33,7 @@ var ( // ErrFunctionNotSupported defines a standardised error for an unsupported // wrapper function by an API ErrFunctionNotSupported = errors.New("unsupported wrapper function") + m sync.Mutex ) // Const declarations for common.go operations @@ -50,10 +52,12 @@ const ( ) func initialiseHTTPClient() { + m.Lock() // If the HTTPClient isn't set, start a new client with a default timeout of 15 seconds if HTTPClient == nil { HTTPClient = NewHTTPClientWithTimeout(time.Second * 15) } + m.Unlock() } // NewHTTPClientWithTimeout initialises a new HTTP client and its underlying @@ -270,8 +274,11 @@ func ExtractHost(address string) string { // ExtractPort returns the port name out of a string func ExtractPort(host string) int { - portStr := strings.Split(host, ":")[1] - port, _ := strconv.Atoi(portStr) + portStrs := strings.Split(host, ":") + if len(portStrs) == 1 { + return 80 + } + port, _ := strconv.Atoi(portStrs[1]) return port } diff --git a/common/common_test.go b/common/common_test.go index 1bc324f4..84d9a51c 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -313,6 +313,14 @@ func TestExtractPort(t *testing.T) { t.Errorf( "Expected '%d'. Actual '%d'.", expectedOutput, actualResult) } + + address = "localhost" + expectedOutput = 80 + actualResult = ExtractPort(address) + if expectedOutput != actualResult { + t.Errorf( + "Expected '%d'. Actual '%d'.", expectedOutput, actualResult) + } } func TestGetURIPath(t *testing.T) { diff --git a/common/gctlogo.png b/common/gctlogo.png new file mode 100644 index 00000000..4cbc4231 Binary files /dev/null and b/common/gctlogo.png differ diff --git a/communications/base/README.md b/communications/base/README.md index 388eafa8..c450bdf8 100644 --- a/communications/base/README.md +++ b/communications/base/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Base - + [![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) diff --git a/communications/base/base.go b/communications/base/base.go index 4090eaa1..330ff284 100644 --- a/communications/base/base.go +++ b/communications/base/base.go @@ -4,18 +4,13 @@ import ( "time" ) -// global vars contain staged update data that will be sent to the communication -// mediums -var ( - ServiceStarted time.Time -) - // Base enforces standard variables across communication packages type Base struct { - Name string - Enabled bool - Verbose bool - Connected bool + Name string + Enabled bool + Verbose bool + Connected bool + ServiceStarted time.Time } // Event is a generalise event type @@ -50,5 +45,80 @@ func (b *Base) GetName() string { func (b *Base) GetStatus() string { return ` GoCryptoTrader Service: Online - Service Started: ` + ServiceStarted.String() + Service Started: ` + b.ServiceStarted.String() +} + +// SetServiceStarted sets the time the service started +func (b *Base) SetServiceStarted(t time.Time) { + b.ServiceStarted = t +} + +// CommunicationsConfig holds all the information needed for each +// enabled communication package +type CommunicationsConfig struct { + SlackConfig SlackConfig `json:"slack"` + SMSGlobalConfig SMSGlobalConfig `json:"smsGlobal"` + SMTPConfig SMTPConfig `json:"smtp"` + TelegramConfig TelegramConfig `json:"telegram"` +} + +// IsAnyEnabled returns whether or any any comms relayers +// are enabled +func (c *CommunicationsConfig) IsAnyEnabled() bool { + if c.SMSGlobalConfig.Enabled || + c.SMTPConfig.Enabled || + c.SlackConfig.Enabled || + c.TelegramConfig.Enabled { + return true + } + return false +} + +// SlackConfig holds all variables to start and run the Slack package +type SlackConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + TargetChannel string `json:"targetChannel"` + VerificationToken string `json:"verificationToken"` +} + +// SMSContact stores the SMS contact info +type SMSContact struct { + Name string `json:"name"` + Number string `json:"number"` + Enabled bool `json:"enabled"` +} + +// SMSGlobalConfig structure holds all the variables you need for instant +// messaging and broadcast used by SMSGlobal +type SMSGlobalConfig struct { + Name string `json:"name"` + From string `json:"from"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + Username string `json:"username"` + Password string `json:"password"` + Contacts []SMSContact `json:"contacts"` +} + +// SMTPConfig holds all variables to start and run the SMTP package +type SMTPConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + Host string `json:"host"` + Port string `json:"port"` + AccountName string `json:"accountName"` + AccountPassword string `json:"accountPassword"` + From string `json:"from"` + RecipientList string `json:"recipientList"` +} + +// TelegramConfig holds all variables to start and run the Telegram package +type TelegramConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + VerificationToken string `json:"verificationToken"` } diff --git a/communications/base/base_interface.go b/communications/base/base_interface.go index 8a90da69..a4a175d5 100644 --- a/communications/base/base_interface.go +++ b/communications/base/base_interface.go @@ -4,7 +4,6 @@ import ( "errors" "time" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -13,18 +12,18 @@ type IComm []ICommunicate // ICommunicate enforces standard functions across communication packages type ICommunicate interface { - Setup(config *config.CommunicationsConfig) + Setup(config *CommunicationsConfig) Connect() error PushEvent(Event) error IsEnabled() bool IsConnected() bool GetName() string + SetServiceStarted(time.Time) } -// Setup sets up communication variables and intiates a connection to the +// Setup sets up communication variables and initiates a connection to the // communication mediums func (c IComm) Setup() { - ServiceStarted = time.Now() for i := range c { if c[i].IsEnabled() && !c[i].IsConnected() { err := c[i].Connect() @@ -33,6 +32,7 @@ func (c IComm) Setup() { continue } log.Debugf(log.CommunicationMgr, "Communications: %v is enabled and online.", c[i].GetName()) + c[i].SetServiceStarted(time.Now()) } } } diff --git a/communications/base/base_test.go b/communications/base/base_test.go index 37f9613f..980fc20b 100644 --- a/communications/base/base_test.go +++ b/communications/base/base_test.go @@ -2,6 +2,7 @@ package base import ( "testing" + "time" ) var ( @@ -35,13 +36,26 @@ func TestGetName(t *testing.T) { } } +func TestSetServiceStarted(t *testing.T) { + b = Base{} + tt := time.Now() + if b.ServiceStarted.Equal(tt) { + t.Errorf("expected '%v', received '%v'", time.Time{}, tt) + } + b.SetServiceStarted(tt) + if !b.ServiceStarted.Equal(tt) { + t.Errorf("expected '%v', received '%v'", tt, b.ServiceStarted) + } +} + type CommunicationProvider struct { ICommunicate - isEnabled bool - isConnected bool - ConnectCalled bool - PushEventCalled bool + isEnabled bool + isConnected bool + ConnectCalled bool + PushEventCalled bool + ServiceStartTime time.Time } func (p *CommunicationProvider) IsEnabled() bool { @@ -66,6 +80,10 @@ func (p *CommunicationProvider) GetName() string { return "someTestProvider" } +func (p *CommunicationProvider) SetServiceStarted(t time.Time) { + p.ServiceStartTime = t +} + func TestSetup(t *testing.T) { var ic IComm testConfigs := []struct { diff --git a/communications/communications.go b/communications/communications.go index 96439b3c..a117e56c 100644 --- a/communications/communications.go +++ b/communications/communications.go @@ -8,7 +8,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/communications/smsglobal" "github.com/thrasher-corp/gocryptotrader/communications/smtpservice" "github.com/thrasher-corp/gocryptotrader/communications/telegram" - "github.com/thrasher-corp/gocryptotrader/config" ) // Communications is the overarching type across the communications packages @@ -16,10 +15,13 @@ type Communications struct { base.IComm } +// ErrNoCommunicationRelayersEnabled returns when no relayers enabled +var ErrNoCommunicationRelayersEnabled = errors.New("no communication relayers enabled") + // NewComm sets up and returns a pointer to a Communications object -func NewComm(cfg *config.CommunicationsConfig) (*Communications, error) { +func NewComm(cfg *base.CommunicationsConfig) (*Communications, error) { if !cfg.IsAnyEnabled() { - return nil, errors.New("no communication relayers enabled") + return nil, ErrNoCommunicationRelayersEnabled } var comm Communications diff --git a/communications/communications_test.go b/communications/communications_test.go index 82717bc6..053239c4 100644 --- a/communications/communications_test.go +++ b/communications/communications_test.go @@ -3,11 +3,11 @@ package communications import ( "testing" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/communications/base" ) func TestNewComm(t *testing.T) { - var cfg config.CommunicationsConfig + var cfg base.CommunicationsConfig _, err := NewComm(&cfg) if err == nil { t.Error("NewComm should have failed on no enabled communication mediums") diff --git a/communications/slack/README.md b/communications/slack/README.md index 12b300ff..6667eb06 100644 --- a/communications/slack/README.md +++ b/communications/slack/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Slack - + [![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) diff --git a/communications/slack/slack.go b/communications/slack/slack.go index e0ed9961..f760beca 100644 --- a/communications/slack/slack.go +++ b/communications/slack/slack.go @@ -15,7 +15,6 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -57,7 +56,7 @@ func (s *Slack) IsConnected() bool { // Setup takes in a slack configuration, sets bots target channel and // sets verification token to access workspace -func (s *Slack) Setup(cfg *config.CommunicationsConfig) { +func (s *Slack) Setup(cfg *base.CommunicationsConfig) { s.Name = cfg.SlackConfig.Name s.Enabled = cfg.SlackConfig.Enabled s.Verbose = cfg.SlackConfig.Verbose @@ -180,7 +179,7 @@ func (s *Slack) NewConnection() error { s.TargetChannel) return s.WebsocketConnect() } - return errors.New("slack.go NewConnection() Already Connected") + return errors.New("newConnection() Already Connected") } // WebsocketConnect creates a websocket dialer amd initiates a websocket diff --git a/communications/smsglobal/README.md b/communications/smsglobal/README.md index 61782734..3dd1544c 100644 --- a/communications/smsglobal/README.md +++ b/communications/smsglobal/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Smsglobal - + [![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) diff --git a/communications/smsglobal/smsglobal.go b/communications/smsglobal/smsglobal.go index cca25039..a32ea9ee 100644 --- a/communications/smsglobal/smsglobal.go +++ b/communications/smsglobal/smsglobal.go @@ -10,7 +10,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -34,7 +33,7 @@ type SMSGlobal struct { // Setup takes in a SMSGlobal configuration, sets username, password and // and recipient list -func (s *SMSGlobal) Setup(cfg *config.CommunicationsConfig) { +func (s *SMSGlobal) Setup(cfg *base.CommunicationsConfig) { s.Name = cfg.SMSGlobalConfig.Name s.Enabled = cfg.SMSGlobalConfig.Enabled s.Verbose = cfg.SMSGlobalConfig.Verbose diff --git a/communications/smtpservice/smtpservice.go b/communications/smtpservice/smtpservice.go index 18bda2e8..6b0a3ae0 100644 --- a/communications/smtpservice/smtpservice.go +++ b/communications/smtpservice/smtpservice.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -29,7 +28,7 @@ type SMTPservice struct { // Setup takes in a SMTP configuration and sets SMTP server details and // recipient list -func (s *SMTPservice) Setup(cfg *config.CommunicationsConfig) { +func (s *SMTPservice) Setup(cfg *base.CommunicationsConfig) { s.Name = cfg.SMTPConfig.Name s.Enabled = cfg.SMTPConfig.Enabled s.Verbose = cfg.SMTPConfig.Verbose diff --git a/communications/telegram/README.md b/communications/telegram/README.md index 288dee7e..35686001 100644 --- a/communications/telegram/README.md +++ b/communications/telegram/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Telegram - + [![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) diff --git a/communications/telegram/telegram.go b/communications/telegram/telegram.go index c1c1bdc5..8d67b96e 100644 --- a/communications/telegram/telegram.go +++ b/communications/telegram/telegram.go @@ -14,7 +14,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -59,7 +58,7 @@ type Telegram struct { func (t *Telegram) IsConnected() bool { return t.Connected } // Setup takes in a Telegram configuration and sets verification token -func (t *Telegram) Setup(cfg *config.CommunicationsConfig) { +func (t *Telegram) Setup(cfg *base.CommunicationsConfig) { t.Name = cfg.TelegramConfig.Name t.Enabled = cfg.TelegramConfig.Enabled t.Token = cfg.TelegramConfig.VerificationToken diff --git a/config/README.md b/config/README.md index 073c6552..b281a964 100644 --- a/config/README.md +++ b/config/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Config - + [![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) diff --git a/config/config.go b/config/config.go index d3c9e18a..943ad213 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/file" + "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/connchecker" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" @@ -171,7 +172,7 @@ func (c *Config) PurgeExchangeAPICredentials() { } // GetCommunicationsConfig returns the communications configuration -func (c *Config) GetCommunicationsConfig() CommunicationsConfig { +func (c *Config) GetCommunicationsConfig() base.CommunicationsConfig { m.Lock() comms := c.Communications m.Unlock() @@ -180,7 +181,7 @@ func (c *Config) GetCommunicationsConfig() CommunicationsConfig { // UpdateCommunicationsConfig sets a new updated version of a Communications // configuration -func (c *Config) UpdateCommunicationsConfig(config *CommunicationsConfig) { +func (c *Config) UpdateCommunicationsConfig(config *base.CommunicationsConfig) { m.Lock() c.Communications = *config m.Unlock() @@ -211,7 +212,7 @@ func (c *Config) CheckCommunicationsConfig() { // with example settings if c.Communications.SlackConfig.Name == "" { - c.Communications.SlackConfig = SlackConfig{ + c.Communications.SlackConfig = base.SlackConfig{ Name: "Slack", TargetChannel: "general", VerificationToken: "testtest", @@ -221,7 +222,7 @@ func (c *Config) CheckCommunicationsConfig() { if c.Communications.SMSGlobalConfig.Name == "" { if c.SMS != nil { if c.SMS.Contacts != nil { - c.Communications.SMSGlobalConfig = SMSGlobalConfig{ + c.Communications.SMSGlobalConfig = base.SMSGlobalConfig{ Name: "SMSGlobal", Enabled: c.SMS.Enabled, Verbose: c.SMS.Verbose, @@ -232,13 +233,13 @@ func (c *Config) CheckCommunicationsConfig() { // flush old SMS config c.SMS = nil } else { - c.Communications.SMSGlobalConfig = SMSGlobalConfig{ + c.Communications.SMSGlobalConfig = base.SMSGlobalConfig{ Name: "SMSGlobal", From: c.Name, Username: "main", Password: "test", - Contacts: []SMSContact{ + Contacts: []base.SMSContact{ { Name: "bob", Number: "1234", @@ -248,12 +249,12 @@ func (c *Config) CheckCommunicationsConfig() { } } } else { - c.Communications.SMSGlobalConfig = SMSGlobalConfig{ + c.Communications.SMSGlobalConfig = base.SMSGlobalConfig{ Name: "SMSGlobal", Username: "main", Password: "test", - Contacts: []SMSContact{ + Contacts: []base.SMSContact{ { Name: "bob", Number: "1234", @@ -279,7 +280,7 @@ func (c *Config) CheckCommunicationsConfig() { } if c.Communications.SMTPConfig.Name == "" { - c.Communications.SMTPConfig = SMTPConfig{ + c.Communications.SMTPConfig = base.SMTPConfig{ Name: "SMTP", Host: "smtp.google.com", Port: "537", @@ -290,7 +291,7 @@ func (c *Config) CheckCommunicationsConfig() { } if c.Communications.TelegramConfig.Name == "" { - c.Communications.TelegramConfig = TelegramConfig{ + c.Communications.TelegramConfig = base.TelegramConfig{ Name: "Telegram", VerificationToken: "testest", } @@ -749,7 +750,7 @@ func (c *Config) GetExchangeConfig(name string) (*ExchangeConfig, error) { return &c.Exchanges[i], nil } } - return nil, fmt.Errorf(ErrExchangeNotFound, name) + return nil, fmt.Errorf("%s %w", name, ErrExchangeNotFound) } // GetForexProvider returns a forex provider configuration by its name @@ -794,7 +795,7 @@ func (c *Config) UpdateExchangeConfig(e *ExchangeConfig) error { return nil } } - return fmt.Errorf(ErrExchangeNotFound, e.Name) + return fmt.Errorf("%s %w", e.Name, ErrExchangeNotFound) } // CheckExchangeConfigValues returns configuation values for all enabled @@ -1345,9 +1346,7 @@ func (c *Config) checkDatabaseConfig() error { database.DB.DataPath = databaseDir } - database.DB.Config = &c.Database - - return nil + return database.DB.SetConfig(&c.Database) } // CheckNTPConfig checks for missing or incorrectly configured NTPClient and recreates with known safe defaults @@ -1371,14 +1370,14 @@ func (c *Config) CheckNTPConfig() { } } -// DisableNTPCheck allows the user to change how they are prompted for timesync alerts -func (c *Config) DisableNTPCheck(input io.Reader) (string, error) { +// SetNTPCheck allows the user to change how they are prompted for timesync alerts +func (c *Config) SetNTPCheck(input io.Reader) (string, error) { m.Lock() defer m.Unlock() reader := bufio.NewReader(input) log.Warnln(log.ConfigMgr, "Your system time is out of sync, this may cause issues with trading") - log.Warnln(log.ConfigMgr, "How would you like to show future notifications? (a)lert / (w)arn / (d)isable") + log.Warnln(log.ConfigMgr, "How would you like to show future notifications? (a)lert at startup / (w)arn periodically / (d)isable") var resp string answered := false @@ -1458,9 +1457,9 @@ func GetAndMigrateDefaultPath(configFile string) (string, error) { // GetFilePath returns the desired config file or the default config file name // and whether it was loaded from a default location (rather than explicitly specified) -func GetFilePath(configfile string) (configPath string, isImplicitDefaultPath bool, err error) { - if configfile != "" { - return configfile, false, nil +func GetFilePath(configFile string) (configPath string, isImplicitDefaultPath bool, err error) { + if configFile != "" { + return configFile, false, nil } exePath, err := common.GetExecutablePath() @@ -1477,16 +1476,16 @@ func GetFilePath(configfile string) (configPath string, isImplicitDefaultPath bo for _, p := range defaultPaths { if file.Exists(p) { - configfile = p + configFile = p break } } - if configfile == "" { + if configFile == "" { return "", false, fmt.Errorf("config.json file not found in %s, please follow README.md in root dir for config generation", newDir) } - return configfile, true, nil + return configFile, true, nil } // migrateConfig will move the config file to the target diff --git a/config/config_test.go b/config/config_test.go index c1cc71f6..c399c576 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -11,13 +11,13 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/file" + "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/connchecker" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctscript "github.com/thrasher-corp/gocryptotrader/gctscript/vm" "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/ntpclient" "github.com/thrasher-corp/gocryptotrader/portfolio/banking" ) @@ -302,7 +302,7 @@ func TestUpdateCommunicationsConfig(t *testing.T) { if err != nil { t.Error("UpdateCommunicationsConfig LoadConfig error", err) } - cfg.UpdateCommunicationsConfig(&CommunicationsConfig{SlackConfig: SlackConfig{Name: testString}}) + cfg.UpdateCommunicationsConfig(&base.CommunicationsConfig{SlackConfig: base.SlackConfig{Name: testString}}) if cfg.Communications.SlackConfig.Name != testString { t.Error("UpdateCommunicationsConfig LoadConfig error") } @@ -340,7 +340,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { t.Error("CheckCommunicationsConfig LoadConfig error", err) } - cfg.Communications = CommunicationsConfig{} + cfg.Communications = base.CommunicationsConfig{} cfg.CheckCommunicationsConfig() if cfg.Communications.SlackConfig.Name != "Slack" || cfg.Communications.SMSGlobalConfig.Name != "SMSGlobal" || @@ -350,14 +350,14 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications) } - cfg.SMS = &SMSGlobalConfig{} + cfg.SMS = &base.SMSGlobalConfig{} cfg.Communications.SMSGlobalConfig.Name = "" cfg.CheckCommunicationsConfig() if cfg.Communications.SMSGlobalConfig.Password != testString { t.Error("CheckCommunicationsConfig error:", err) } - cfg.SMS.Contacts = append(cfg.SMS.Contacts, SMSContact{ + cfg.SMS.Contacts = append(cfg.SMS.Contacts, base.SMSContact{ Name: "Bobby", Number: "4321", Enabled: false, @@ -380,7 +380,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { t.Error("CheckCommunicationsConfig From value should have been trimmed to 11 characters") } - cfg.SMS = &SMSGlobalConfig{} + cfg.SMS = &base.SMSGlobalConfig{} cfg.CheckCommunicationsConfig() if cfg.SMS != nil { t.Error("CheckCommunicationsConfig unexpected data:", @@ -1883,7 +1883,7 @@ func TestDisableNTPCheck(t *testing.T) { var c Config - warn, err := c.DisableNTPCheck(strings.NewReader("w\n")) + warn, err := c.SetNTPCheck(strings.NewReader("w\n")) if err != nil { t.Fatalf("to create ntpclient failed reason: %v", err) } @@ -1891,17 +1891,17 @@ func TestDisableNTPCheck(t *testing.T) { if warn != "Time sync has been set to warn only" { t.Errorf("failed expected %v got %v", "Time sync has been set to warn only", warn) } - alert, _ := c.DisableNTPCheck(strings.NewReader("a\n")) + alert, _ := c.SetNTPCheck(strings.NewReader("a\n")) if alert != "Time sync has been set to alert" { t.Errorf("failed expected %v got %v", "Time sync has been set to alert", alert) } - disable, _ := c.DisableNTPCheck(strings.NewReader("d\n")) + disable, _ := c.SetNTPCheck(strings.NewReader("d\n")) if disable != "Future notifications for out of time sync has been disabled" { t.Errorf("failed expected %v got %v", "Future notifications for out of time sync has been disabled", disable) } - _, err = c.DisableNTPCheck(strings.NewReader(" ")) + _, err = c.SetNTPCheck(strings.NewReader(" ")) if err.Error() != "EOF" { t.Errorf("failed expected EOF got: %v", err) } @@ -1960,7 +1960,6 @@ func TestCheckNTPConfig(t *testing.T) { c.NTPClient.AllowedDifference = nil c.CheckNTPConfig() - _ = ntpclient.NTPClient(c.NTPClient.Pool) if c.NTPClient.Pool[0] != "pool.ntp.org:123" { t.Error("ntpclient with no valid pool should default to pool.ntp.org") diff --git a/config/config_types.go b/config/config_types.go index 87e4b999..9a966508 100644 --- a/config/config_types.go +++ b/config/config_types.go @@ -1,9 +1,11 @@ package config import ( + "errors" "sync" "time" + "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" @@ -39,17 +41,9 @@ const ( // Constants here hold some messages const ( ErrExchangeNameEmpty = "exchange #%d name is empty" - ErrExchangeAvailablePairsEmpty = "exchange %s available pairs is empty" - ErrExchangeEnabledPairsEmpty = "exchange %s enabled pairs is empty" - ErrExchangeBaseCurrenciesEmpty = "exchange %s base currencies is empty" - ErrExchangeNotFound = "exchange %s not found" ErrNoEnabledExchanges = "no exchanges enabled" - ErrCryptocurrenciesEmpty = "cryptocurrencies variable is empty" ErrFailureOpeningConfig = "fatal error opening %s file. Error: %s" ErrCheckingConfigValues = "fatal error checking config values. Error: %s" - ErrSavingConfigBytesMismatch = "config file %q bytes comparison doesn't match, read %s expected %s" - WarningWebserverCredentialValuesEmpty = "webserver support disabled due to empty Username/Password values" - WarningWebserverListenAddressInvalid = "webserver support disabled due to invalid listen address" WarningExchangeAuthAPIDefaultOrEmptyValues = "exchange %s authenticated API support disabled due to default/empty APIKey/Secret/ClientID values" WarningPairsLastUpdatedThresholdExceeded = "exchange %s last manual update of available currency pairs has exceeded %d days. Manual update required!" ) @@ -67,37 +61,38 @@ const ( // Variables here are used for configuration var ( - Cfg Config - m sync.Mutex + Cfg Config + m sync.Mutex + ErrExchangeNotFound = errors.New("exchange not found") ) // Config is the overarching object that holds all the information for // prestart management of Portfolio, Communications, Webserver and Enabled // Exchanges type Config struct { - Name string `json:"name"` - DataDirectory string `json:"dataDirectory"` - EncryptConfig int `json:"encryptConfig"` - GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` - Database database.Config `json:"database"` - Logging log.Config `json:"logging"` - ConnectionMonitor ConnectionMonitorConfig `json:"connectionMonitor"` - Profiler Profiler `json:"profiler"` - NTPClient NTPClientConfig `json:"ntpclient"` - GCTScript gctscript.Config `json:"gctscript"` - Currency CurrencyConfig `json:"currencyConfig"` - Communications CommunicationsConfig `json:"communications"` - RemoteControl RemoteControlConfig `json:"remoteControl"` - Portfolio portfolio.Base `json:"portfolioAddresses"` - Exchanges []ExchangeConfig `json:"exchanges"` - BankAccounts []banking.Account `json:"bankAccounts"` + Name string `json:"name"` + DataDirectory string `json:"dataDirectory"` + EncryptConfig int `json:"encryptConfig"` + GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` + Database database.Config `json:"database"` + Logging log.Config `json:"logging"` + ConnectionMonitor ConnectionMonitorConfig `json:"connectionMonitor"` + Profiler Profiler `json:"profiler"` + NTPClient NTPClientConfig `json:"ntpclient"` + GCTScript gctscript.Config `json:"gctscript"` + Currency CurrencyConfig `json:"currencyConfig"` + Communications base.CommunicationsConfig `json:"communications"` + RemoteControl RemoteControlConfig `json:"remoteControl"` + Portfolio portfolio.Base `json:"portfolioAddresses"` + Exchanges []ExchangeConfig `json:"exchanges"` + BankAccounts []banking.Account `json:"bankAccounts"` // Deprecated config settings, will be removed at a future date Webserver *WebserverConfig `json:"webserver,omitempty"` CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat,omitempty"` FiatDisplayCurrency *currency.Code `json:"fiatDispayCurrency,omitempty"` Cryptocurrencies *currency.Currencies `json:"cryptocurrencies,omitempty"` - SMS *SMSGlobalConfig `json:"smsGlobal,omitempty"` + SMS *base.SMSGlobalConfig `json:"smsGlobal,omitempty"` // encryption session values storedSalt []byte sessionDK []byte @@ -251,76 +246,6 @@ type CryptocurrencyProvider struct { AccountPlan string `json:"accountPlan"` } -// CommunicationsConfig holds all the information needed for each -// enabled communication package -type CommunicationsConfig struct { - SlackConfig SlackConfig `json:"slack"` - SMSGlobalConfig SMSGlobalConfig `json:"smsGlobal"` - SMTPConfig SMTPConfig `json:"smtp"` - TelegramConfig TelegramConfig `json:"telegram"` -} - -// IsAnyEnabled returns whether or any any comms relayers -// are enabled -func (c *CommunicationsConfig) IsAnyEnabled() bool { - if c.SMSGlobalConfig.Enabled || - c.SMTPConfig.Enabled || - c.SlackConfig.Enabled || - c.TelegramConfig.Enabled { - return true - } - return false -} - -// SlackConfig holds all variables to start and run the Slack package -type SlackConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - TargetChannel string `json:"targetChannel"` - VerificationToken string `json:"verificationToken"` -} - -// SMSContact stores the SMS contact info -type SMSContact struct { - Name string `json:"name"` - Number string `json:"number"` - Enabled bool `json:"enabled"` -} - -// SMSGlobalConfig structure holds all the variables you need for instant -// messaging and broadcast used by SMSGlobal -type SMSGlobalConfig struct { - Name string `json:"name"` - From string `json:"from"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - Username string `json:"username"` - Password string `json:"password"` - Contacts []SMSContact `json:"contacts"` -} - -// SMTPConfig holds all variables to start and run the SMTP package -type SMTPConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - Host string `json:"host"` - Port string `json:"port"` - AccountName string `json:"accountName"` - AccountPassword string `json:"accountPassword"` - From string `json:"from"` - RecipientList string `json:"recipientList"` -} - -// TelegramConfig holds all variables to start and run the Telegram package -type TelegramConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - VerificationToken string `json:"verificationToken"` -} - // FeaturesSupportedConfig stores the exchanges supported features type FeaturesSupportedConfig struct { REST bool `json:"restAPI"` diff --git a/connchecker/connchecker.go b/connchecker/connchecker.go index c2d01e35..a8952ad3 100644 --- a/connchecker/connchecker.go +++ b/connchecker/connchecker.go @@ -79,6 +79,7 @@ type Checker struct { // Shutdown cleanly shutsdown monitor routine func (c *Checker) Shutdown() { + c.connected = false close(c.shutdown) c.wg.Wait() } diff --git a/currency/README.md b/currency/README.md index e8258a42..b19d4670 100644 --- a/currency/README.md +++ b/currency/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Currency - + [![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) diff --git a/currency/forexprovider/README.md b/currency/forexprovider/README.md index d0db0ea4..930bff13 100644 --- a/currency/forexprovider/README.md +++ b/currency/forexprovider/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Forexprovider - + [![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) diff --git a/currency/forexprovider/base/README.md b/currency/forexprovider/base/README.md index 4b2db5c8..d5ce1c3f 100644 --- a/currency/forexprovider/base/README.md +++ b/currency/forexprovider/base/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Base - + [![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) diff --git a/currency/forexprovider/currencyconverterapi/README.md b/currency/forexprovider/currencyconverterapi/README.md index 98cae06f..88de0a4f 100644 --- a/currency/forexprovider/currencyconverterapi/README.md +++ b/currency/forexprovider/currencyconverterapi/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Currencyconverterapi - + [![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) diff --git a/currency/forexprovider/currencylayer/README.md b/currency/forexprovider/currencylayer/README.md index 92226517..dac480eb 100644 --- a/currency/forexprovider/currencylayer/README.md +++ b/currency/forexprovider/currencylayer/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Currencylayer - + [![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) diff --git a/currency/forexprovider/exchangerate.host/README.md b/currency/forexprovider/exchangerate.host/README.md index c20a68c3..c5776881 100644 --- a/currency/forexprovider/exchangerate.host/README.md +++ b/currency/forexprovider/exchangerate.host/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Exchangerate.Host - + [![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) diff --git a/currency/forexprovider/exchangeratesapi.io/README.md b/currency/forexprovider/exchangeratesapi.io/README.md index 37307e4b..5e913a43 100644 --- a/currency/forexprovider/exchangeratesapi.io/README.md +++ b/currency/forexprovider/exchangeratesapi.io/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Exchangeratesapi.Io - + [![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) diff --git a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go index b461198a..22a7ea2d 100644 --- a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go +++ b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go @@ -166,11 +166,11 @@ func (e *ExchangeRates) GetTimeSeriesRates(startDate, endDate time.Time, baseCur } if startDate.IsZero() || endDate.IsZero() { - return nil, errors.New("startDate and endDate params must be set") + return nil, errStartEndDatesInvalid } if startDate.After(endDate) { - return nil, errors.New("startDate must be before endDate") + return nil, errStartAfterEnd } v := url.Values{} @@ -197,11 +197,11 @@ func (e *ExchangeRates) GetFluctuations(startDate, endDate time.Time, baseCurren } if startDate.IsZero() || endDate.IsZero() { - return nil, errors.New("startDate and endDate must be set") + return nil, errStartEndDatesInvalid } if startDate.After(endDate) { - return nil, errors.New("startDate must be before endDate") + return nil, errStartAfterEnd } v := url.Values{} diff --git a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_test.go b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_test.go index 7ab87081..f21f005c 100644 --- a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_test.go +++ b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_test.go @@ -147,14 +147,14 @@ func TestGetTimeSeriesRates(t *testing.T) { } _, err := e.GetTimeSeriesRates(time.Time{}, time.Time{}, "USD", []string{"EUR", "USD"}) - if err == nil { - t.Fatal("empty startDate endDate params should throw an error") + if !errors.Is(err, errStartEndDatesInvalid) { + t.Fatalf("received '%v' expected '%v'", err, errStartEndDatesInvalid) } tmNow := time.Now() _, err = e.GetTimeSeriesRates(tmNow.AddDate(0, 1, 0), tmNow, "USD", []string{"EUR", "USD"}) - if err == nil { - t.Fatal("future startTime should throw an error") + if !errors.Is(err, errStartAfterEnd) { + t.Fatalf("received '%v' expected '%v'", err, errStartAfterEnd) } _, err = e.GetTimeSeriesRates(tmNow.AddDate(0, -1, 0), tmNow, "EUR", []string{"AUD,USD"}) diff --git a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go index 83a784fe..598e9589 100644 --- a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go +++ b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go @@ -28,6 +28,8 @@ const ( var ( errCannotSetBaseCurrencyOnFreePlan = errors.New("base currency cannot be set on the free plan") errAPIKeyLevelRestrictedAccess = errors.New("apiKey level function access denied") + errStartEndDatesInvalid = errors.New("startDate and endDate params must be set") + errStartAfterEnd = errors.New("startDate must be before endDate") ) // ExchangeRates stores the struct for the ExchangeRatesAPI API diff --git a/currency/forexprovider/fixer.io/README.md b/currency/forexprovider/fixer.io/README.md index 9ac15b4e..a358488d 100644 --- a/currency/forexprovider/fixer.io/README.md +++ b/currency/forexprovider/fixer.io/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Fixer.Io - + [![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) diff --git a/currency/forexprovider/openexchangerates/README.md b/currency/forexprovider/openexchangerates/README.md index 74051c64..4417b2a4 100644 --- a/currency/forexprovider/openexchangerates/README.md +++ b/currency/forexprovider/openexchangerates/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Openexchangerates - + [![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) diff --git a/database/database.go b/database/database.go new file mode 100644 index 00000000..3ed3b2be --- /dev/null +++ b/database/database.go @@ -0,0 +1,96 @@ +package database + +import ( + "database/sql" + "time" + + "github.com/thrasher-corp/sqlboiler/boil" +) + +// SetConfig safely sets the global database instance's config with some +// basic locks and checks +func (i *Instance) SetConfig(cfg *Config) error { + if i == nil { + return errNilInstance + } + if cfg == nil { + return errNilConfig + } + i.m.Lock() + i.config = cfg + if i.config.Verbose { + boil.DebugMode = true + boil.DebugWriter = Logger{} + } else { + boil.DebugMode = false + } + i.m.Unlock() + return nil +} + +// SetSQLiteConnection safely sets the global database instance's connection +// to use SQLite +func (i *Instance) SetSQLiteConnection(con *sql.DB) { + i.m.Lock() + defer i.m.Unlock() + i.SQL = con + i.SQL.SetMaxOpenConns(1) +} + +// SetPostgresConnection safely sets the global database instance's connection +// to use Postgres +func (i *Instance) SetPostgresConnection(con *sql.DB) error { + if err := con.Ping(); err != nil { + return err + } + i.m.Lock() + defer i.m.Unlock() + i.SQL = con + i.SQL.SetMaxOpenConns(2) + i.SQL.SetMaxIdleConns(1) + i.SQL.SetConnMaxLifetime(time.Hour) + return nil +} + +// SetConnected safely sets the global database instance's connected +// status +func (i *Instance) SetConnected(v bool) { + i.m.Lock() + i.connected = v + i.m.Unlock() +} + +// CloseConnection safely disconnects the global database instance +func (i *Instance) CloseConnection() error { + i.m.Lock() + defer i.m.Unlock() + return i.SQL.Close() +} + +// IsConnected safely checks the SQL connection status +func (i *Instance) IsConnected() bool { + i.m.RLock() + defer i.m.RUnlock() + return i.connected +} + +// GetConfig safely returns a copy of the config +func (i *Instance) GetConfig() *Config { + i.m.RLock() + defer i.m.RUnlock() + cpy := i.config + return cpy +} + +// Ping pings the database +func (i *Instance) Ping() error { + if i == nil { + return errNilInstance + } + i.m.RLock() + defer i.m.RUnlock() + if i.SQL == nil { + return errNilSQL + } + return i.SQL.Ping() +} diff --git a/database/database_types.go b/database/database_types.go index 432f1d0c..cd5efe62 100644 --- a/database/database_types.go +++ b/database/database_types.go @@ -13,9 +13,9 @@ import ( type Instance struct { SQL *sql.DB DataPath string - Config *Config - Connected bool - Mu sync.RWMutex + config *Config + connected bool + m sync.RWMutex } // Config holds all database configurable options including enable/disabled & DSN settings @@ -29,21 +29,21 @@ type Config struct { var ( // DB Global Database Connection DB = &Instance{} - // MigrationDir which folder to look in for current migrations MigrationDir = filepath.Join("..", "..", "database", "migrations") - // ErrNoDatabaseProvided error to display when no database is provided ErrNoDatabaseProvided = errors.New("no database provided") - // ErrDatabaseSupportDisabled error to display when no database is provided ErrDatabaseSupportDisabled = errors.New("database support is disabled") - // SupportedDrivers slice of supported database driver types SupportedDrivers = []string{DBSQLite, DBSQLite3, DBPostgreSQL} - + // ErrFailedToConnect for when a database fails to connect + ErrFailedToConnect = errors.New("database failed to connect") // DefaultSQLiteDatabase is the default sqlite3 database name to use DefaultSQLiteDatabase = "gocryptotrader.db" + errNilConfig = errors.New("received nil config") + errNilInstance = errors.New("database instance is nil") + errNilSQL = errors.New("database SQL connection is nil") ) const ( diff --git a/database/drivers/postgres/postgres.go b/database/drivers/postgres/postgres.go index f74fc049..fa5e3338 100644 --- a/database/drivers/postgres/postgres.go +++ b/database/drivers/postgres/postgres.go @@ -3,7 +3,6 @@ package postgres import ( "database/sql" "fmt" - "time" // import go libpq driver package _ "github.com/lib/pq" @@ -12,32 +11,27 @@ import ( // Connect opens a connection to Postgres database and returns a pointer to database.DB func Connect() (*database.Instance, error) { - if database.DB.Config.SSLMode == "" { - database.DB.Config.SSLMode = "disable" + cfg := database.DB.GetConfig() + + if cfg.SSLMode == "" { + cfg.SSLMode = "disable" } configDSN := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", - database.DB.Config.Username, - database.DB.Config.Password, - database.DB.Config.Host, - database.DB.Config.Port, - database.DB.Config.Database, - database.DB.Config.SSLMode) + cfg.Username, + cfg.Password, + cfg.Host, + cfg.Port, + cfg.Database, + cfg.SSLMode) db, err := sql.Open(database.DBPostgreSQL, configDSN) if err != nil { return nil, err } - - err = db.Ping() + err = database.DB.SetPostgresConnection(db) if err != nil { return nil, err } - - database.DB.SQL = db - database.DB.SQL.SetMaxOpenConns(2) - database.DB.SQL.SetMaxIdleConns(1) - database.DB.SQL.SetConnMaxLifetime(time.Hour) - return database.DB, nil } diff --git a/database/drivers/sqlite3/sqlite3.go b/database/drivers/sqlite3/sqlite3.go index da1a5023..9ea5a180 100644 --- a/database/drivers/sqlite3/sqlite3.go +++ b/database/drivers/sqlite3/sqlite3.go @@ -11,19 +11,19 @@ import ( // Connect opens a connection to sqlite database and returns a pointer to database.DB func Connect() (*database.Instance, error) { - if database.DB.Config.Database == "" { + cfg := database.DB.GetConfig() + if cfg.Database == "" { return nil, database.ErrNoDatabaseProvided } - databaseFullLocation := filepath.Join(database.DB.DataPath, database.DB.Config.Database) + databaseFullLocation := filepath.Join(database.DB.DataPath, cfg.Database) dbConn, err := sql.Open("sqlite3", databaseFullLocation) if err != nil { return nil, err } - database.DB.SQL = dbConn - database.DB.SQL.SetMaxOpenConns(1) + database.DB.SetSQLiteConnection(dbConn) return database.DB, nil } diff --git a/database/repository/repository.go b/database/repository/repository.go index ed2fab36..28235b3e 100644 --- a/database/repository/repository.go +++ b/database/repository/repository.go @@ -6,7 +6,8 @@ import ( // GetSQLDialect returns current SQL Dialect based on enabled driver func GetSQLDialect() string { - switch database.DB.Config.Driver { + cfg := database.DB.GetConfig() + switch cfg.Driver { case "sqlite", "sqlite3": return database.DBSQLite3 case "psql", "postgres", "postgresql": diff --git a/database/repository/repository_test.go b/database/repository/repository_test.go index 140c0f0b..d917205d 100644 --- a/database/repository/repository_test.go +++ b/database/repository/repository_test.go @@ -32,8 +32,11 @@ func TestGetSQLDialect(t *testing.T) { test := testCases[x] t.Run(test.driver, func(t *testing.T) { - database.DB.Config = &database.Config{ + err := database.DB.SetConfig(&database.Config{ Driver: test.driver, + }) + if err != nil { + t.Error(err) } ret := GetSQLDialect() if ret != test.expectedReturn { diff --git a/database/testhelpers/test_helpers.go b/database/testhelpers/test_helpers.go index ad99c355..d27c8b6d 100644 --- a/database/testhelpers/test_helpers.go +++ b/database/testhelpers/test_helpers.go @@ -75,7 +75,10 @@ func GetConnectionDetails() *database.Config { // ConnectToDatabase opens connection to database and returns pointer to instance of database.DB func ConnectToDatabase(conn *database.Config) (dbConn *database.Instance, err error) { - database.DB.Config = conn + err = database.DB.SetConfig(conn) + if err != nil { + return nil, err + } if conn.Driver == database.DBPostgreSQL { dbConn, err = psqlConn.Connect() if err != nil { diff --git a/dispatch/dispatch.go b/dispatch/dispatch.go index ab26f789..d5e79ebd 100644 --- a/dispatch/dispatch.go +++ b/dispatch/dispatch.go @@ -8,10 +8,12 @@ import ( "time" "github.com/gofrs/uuid" - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" "github.com/thrasher-corp/gocryptotrader/log" ) +// Name is an exported subsystem name +const Name = "dispatch" + func init() { dispatcher = &Dispatcher{ routes: make(map[uuid.UUID][]chan interface{}), @@ -80,7 +82,7 @@ func SpawnWorker() error { // configuration, then spawns workers func (d *Dispatcher) start(workers, channelCapacity int) error { if atomic.LoadUint32(&d.running) == 1 { - return fmt.Errorf("dispatcher %w", subsystem.ErrSubSystemAlreadyStarted) + return errors.New("dispatcher already running") } if workers < 1 { @@ -115,7 +117,7 @@ func (d *Dispatcher) start(workers, channelCapacity int) error { // stop stops the service and shuts down all worker routines func (d *Dispatcher) stop() error { if !atomic.CompareAndSwapUint32(&d.running, 1, 0) { - return fmt.Errorf("dispatcher %w", subsystem.ErrSubSystemNotStarted) + return errors.New("dispatcher not running") } close(d.shutdown) ch := make(chan struct{}) @@ -154,6 +156,9 @@ func (d *Dispatcher) stop() error { // isRunning returns if the dispatch system is running func (d *Dispatcher) isRunning() bool { + if d == nil { + return false + } return atomic.LoadUint32(&d.running) == 1 } diff --git a/dispatch/dispatch_types.go b/dispatch/dispatch_types.go index c1b7029c..1ab4c221 100644 --- a/dispatch/dispatch_types.go +++ b/dispatch/dispatch_types.go @@ -66,7 +66,7 @@ type job struct { ID uuid.UUID } -// Mux defines a new multiplexor for the dispatch system, these a generated +// Mux defines a new multiplexer for the dispatch system, these a generated // per subsystem type Mux struct { // Reference to the main running dispatch service @@ -80,6 +80,6 @@ type Pipe struct { C chan interface{} // ID to tracked system id uuid.UUID - // Reference to multiplexor + // Reference to multiplexer m *Mux } diff --git a/dispatch/mux.go b/dispatch/mux.go index 38037403..238cb56a 100644 --- a/dispatch/mux.go +++ b/dispatch/mux.go @@ -7,7 +7,7 @@ import ( "github.com/gofrs/uuid" ) -// GetNewMux returns a new multiplexor to track subsystem updates +// GetNewMux returns a new multiplexer to track subsystem updates func GetNewMux() *Mux { return &Mux{d: dispatcher} } diff --git a/engine/addr_helpers.go b/engine/addr_helpers.go deleted file mode 100644 index 533f3b57..00000000 --- a/engine/addr_helpers.go +++ /dev/null @@ -1,100 +0,0 @@ -package engine - -import ( - "errors" - "strings" - "sync" - - "github.com/thrasher-corp/gocryptotrader/currency" -) - -// DepositAddressStore stores a list of exchange deposit addresses -type DepositAddressStore struct { - m sync.Mutex - Store map[string]map[string]string -} - -// DepositAddressManager manages the exchange deposit address store -type DepositAddressManager struct { - Store DepositAddressStore -} - -// vars related to the deposit address helpers -var ( - ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil") - ErrDepositAddressNotFound = errors.New("deposit address does not exist") -) - -// Seed seeds the deposit address store -func (d *DepositAddressStore) Seed(coinData map[string]map[string]string) { - d.m.Lock() - defer d.m.Unlock() - if d.Store == nil { - d.Store = make(map[string]map[string]string) - } - - for k, v := range coinData { - r := make(map[string]string) - for w, x := range v { - r[strings.ToUpper(w)] = x - } - d.Store[strings.ToUpper(k)] = r - } -} - -// GetDepositAddress returns a deposit address based on the specified item -func (d *DepositAddressStore) GetDepositAddress(exchName string, item currency.Code) (string, error) { - d.m.Lock() - defer d.m.Unlock() - - if len(d.Store) == 0 { - return "", ErrDepositAddressStoreIsNil - } - - r, ok := d.Store[strings.ToUpper(exchName)] - if !ok { - return "", ErrExchangeNotFound - } - - addr, ok := r[strings.ToUpper(item.String())] - if !ok { - return "", ErrDepositAddressNotFound - } - - return addr, nil -} - -// GetDepositAddresses returns a list of stored deposit addresses -func (d *DepositAddressStore) GetDepositAddresses(exchName string) (map[string]string, error) { - d.m.Lock() - defer d.m.Unlock() - - if len(d.Store) == 0 { - return nil, ErrDepositAddressStoreIsNil - } - - r, ok := d.Store[strings.ToUpper(exchName)] - if !ok { - return nil, ErrDepositAddressNotFound - } - - return r, nil -} - -// GetDepositAddressByExchange returns a deposit address for the specified exchange and cryptocurrency -// if it exists -func (d *DepositAddressManager) GetDepositAddressByExchange(exchName string, currencyItem currency.Code) (string, error) { - return d.Store.GetDepositAddress(exchName, currencyItem) -} - -// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified -// exchange if they exist -func (d *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string]string, error) { - return d.Store.GetDepositAddresses(exchName) -} - -// Sync synchronises all deposit addresses -func (d *DepositAddressManager) Sync() { - result := Bot.GetExchangeCryptocurrencyDepositAddresses() - d.Store.Seed(result) -} diff --git a/engine/addr_helpers_test.go b/engine/addr_helpers_test.go deleted file mode 100644 index d3405e4d..00000000 --- a/engine/addr_helpers_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package engine - -import ( - "testing" - - "github.com/thrasher-corp/gocryptotrader/currency" -) - -const ( - testBTCAddress = "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" -) - -func TestSeed(t *testing.T) { - var d DepositAddressStore - u := map[string]map[string]string{ - "BITSTAMP": { - "BTC": testBTCAddress, - }, - } - - d.Seed(u) - r, err := d.GetDepositAddress("BITSTAMP", currency.BTC) - if err != nil { - t.Error("unexpected result") - } - - if r != testBTCAddress { - t.Error("unexpected result") - } -} - -func TestGetDepositAddress(t *testing.T) { - var d DepositAddressStore - _, err := d.GetDepositAddress("", currency.BTC) - if err != ErrDepositAddressStoreIsNil { - t.Error("non-error on non-existent exchange") - } - - d.Store = map[string]map[string]string{ - "BITSTAMP": { - "BTC": testBTCAddress, - }, - } - - _, err = d.GetDepositAddress("", currency.BTC) - if err != ErrExchangeNotFound { - t.Error("non-error on non-existent exchange") - } - - var r string - r, err = d.GetDepositAddress("BiTStAmP", currency.NewCode("bTC")) - if err != nil { - t.Error("unexpected err: ", err) - } - - if r != testBTCAddress { - t.Error("unexpected BTC address: ", r) - } - - _, err = d.GetDepositAddress("BiTStAmP", currency.LTC) - if err != ErrDepositAddressNotFound { - t.Error("unexpected err: ", err) - } -} diff --git a/engine/apiserver.go b/engine/apiserver.go new file mode 100644 index 00000000..3e8286ab --- /dev/null +++ b/engine/apiserver.go @@ -0,0 +1,906 @@ +package engine + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/pprof" + "runtime" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/database/repository/exchange" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "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.ExtractPort(m.websocketListenAddress) == 80 { + m.websocketListenAddress = common.ExtractHost(m.websocketListenAddress) + } else { + m.websocketListenAddress = strings.Join([]string{common.ExtractHost(m.websocketListenAddress), + strconv.Itoa(common.ExtractPort(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.ExtractHost(m.websocketListenAddress), + common.ExtractPort(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.ExtractHost(m.restListenAddress), common.ExtractPort(m.restListenAddress)) + m.restRouter = m.newRouter(true) + if m.restHTTPServer == nil { + m.restHTTPServer = &http.Server{ + Addr: m.restListenAddress, + Handler: m.restRouter, + } + } + m.wgRest.Add(1) + go func() { + defer m.wgRest.Done() + err := m.restHTTPServer.ListenAndServe() + if err != nil { + atomic.StoreInt32(&m.restStarted, 0) + if !errors.Is(err, http.ErrServerClosed) { + log.Error(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 interface{}) 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.Error(log.APIServerMgr, err) + } + w.WriteHeader(http.StatusOK) +} + +// getAllActiveOrderbooks returns all enabled exchanges orderbooks +func getAllActiveOrderbooks(m iExchangeManager) []EnabledExchangeOrderbooks { + var orderbookData []EnabledExchangeOrderbooks + exchanges := m.GetExchanges() + for x := range exchanges { + assets := exchanges[x].GetAssetTypes() + exchName := exchanges[x].GetName() + var exchangeOB EnabledExchangeOrderbooks + exchangeOB.ExchangeName = exchName + + for y := range assets { + currencies, err := exchanges[x].GetEnabledPairs(assets[y]) + if err != nil { + log.Errorf(log.APIServerMgr, + "Exchange %s could not retrieve enabled currencies. Err: %s\n", + exchName, + err) + continue + } + for z := range currencies { + ob, err := exchanges[x].FetchOrderbook(currencies[z], assets[y]) + if err != nil { + log.Errorf(log.APIServerMgr, + "Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName, + currencies[z].String(), + err) + continue + } + exchangeOB.ExchangeValues = append(exchangeOB.ExchangeValues, *ob) + } + orderbookData = append(orderbookData, exchangeOB) + } + orderbookData = append(orderbookData, exchangeOB) + } + return orderbookData +} + +// getAllActiveTickers returns all enabled exchanges tickers +func getAllActiveTickers(m iExchangeManager) []EnabledExchangeCurrencies { + var tickers []EnabledExchangeCurrencies + exchanges := m.GetExchanges() + for x := range exchanges { + assets := exchanges[x].GetAssetTypes() + exchName := exchanges[x].GetName() + var exchangeTickers EnabledExchangeCurrencies + exchangeTickers.ExchangeName = exchName + + for y := range assets { + currencies, err := exchanges[x].GetEnabledPairs(assets[y]) + if err != nil { + log.Errorf(log.APIServerMgr, + "Exchange %s could not retrieve enabled currencies. Err: %s\n", + exchName, + err) + continue + } + for z := range currencies { + t, err := exchanges[x].FetchTicker(currencies[z], assets[y]) + if err != nil { + log.Errorf(log.APIServerMgr, + "Exchange %s failed to retrieve %s ticker. Err: %s\n", exchName, + currencies[z].String(), + err) + continue + } + exchangeTickers.ExchangeValues = append(exchangeTickers.ExchangeValues, *t) + } + tickers = append(tickers, exchangeTickers) + } + tickers = append(tickers, exchangeTickers) + } + return tickers +} + +// getAllActiveAccounts returns all enabled exchanges accounts +func getAllActiveAccounts(m iExchangeManager) []AllEnabledExchangeAccounts { + var accounts []AllEnabledExchangeAccounts + exchanges := m.GetExchanges() + for x := range exchanges { + assets := exchanges[x].GetAssetTypes() + exchName := exchanges[x].GetName() + var exchangeAccounts AllEnabledExchangeAccounts + for y := range assets { + a, err := exchanges[x].FetchAccountInfo(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.ExtractHost(m.websocketListenAddress), common.ExtractPort(m.websocketListenAddress)) + m.websocketRouter = m.newRouter(false) + if m.websocketHTTPServer == nil { + m.websocketHTTPServer = &http.Server{ + Addr: m.websocketListenAddress, + Handler: m.websocketRouter, + } + } + + m.wgWebsocket.Add(1) + go func() { + defer m.wgWebsocket.Done() + err := m.websocketHTTPServer.ListenAndServe() + if err != nil { + atomic.StoreInt32(&m.websocketStarted, 0) + if !errors.Is(err, http.ErrServerClosed) { + log.Error(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 interface{}) 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.Error(log.APIServerMgr, conErr) + } + }() + + for { + msgType, message, err := c.Conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Errorf(log.APIServerMgr, "websocket: client disconnected, err: %s\n", err) + } + break + } + + if msgType == websocket.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.Error(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.Error(log.APIServerMgr, err) + } + }() + for { + message, ok := <-c.Send + if !ok { + err := c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + if err != nil { + log.Error(log.APIServerMgr, err) + } + log.Debugln(log.APIServerMgr, "websocket: hub closed the channel") + return + } + + w, err := c.Conn.NextWriter(websocket.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.Error(log.APIServerMgr, err) + } + + // Add queued chat messages to the current websocket message + n := len(c.Send) + for i := 0; i < n; i++ { + _, err = w.Write(<-c.Send) + if err != nil { + log.Error(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 := websocket.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(r *http.Request) bool { return true } + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error(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 interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "auth", + } + + var auth WebsocketAuth + err := json.Unmarshal(data.([]byte), &auth) + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(log.APIServerMgr, sendErr) + } + return err + } + + hashPW := crypto.HexEncodeToString(crypto.GetSHA256([]byte(client.password))) + if auth.Username == client.username && auth.Password == hashPW { + 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.Error(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, _ interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetConfig", + Data: config.GetConfig(), + } + return client.SendWebsocketMessage(wsResp) +} + +func wsSaveConfig(client *websocketClient, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "SaveConfig", + } + var respCfg config.Config + err := json.Unmarshal(data.([]byte), &respCfg) + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(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.Error(log.APIServerMgr, sendErr) + } + return err + } + + err = client.bot.SetupExchanges() + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(log.APIServerMgr, sendErr) + } + return err + } + wsResp.Data = WebsocketResponseSuccess + return client.SendWebsocketMessage(wsResp) +} + +func wsGetAccountInfo(client *websocketClient, data interface{}) error { + accountInfo := getAllActiveAccounts(client.exchangeManager) + wsResp := WebsocketEventResponse{ + Event: "GetAccountInfo", + Data: accountInfo, + } + return client.SendWebsocketMessage(wsResp) +} + +func wsGetTickers(client *websocketClient, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetTickers", + } + wsResp.Data = getAllActiveTickers(client.exchangeManager) + return client.SendWebsocketMessage(wsResp) +} + +func wsGetTicker(client *websocketClient, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetTicker", + } + var tickerReq WebsocketOrderbookTickerRequest + err := json.Unmarshal(data.([]byte), &tickerReq) + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(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 := client.exchangeManager.GetExchangeByName(tickerReq.Exchange) + if exch == nil { + wsResp.Error = exchange.ErrNoExchangeFound.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(log.APIServerMgr, sendErr) + } + return err + } + tick, err := exch.FetchTicker(p, a) + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(log.APIServerMgr, sendErr) + } + return err + } + wsResp.Data = tick + return client.SendWebsocketMessage(wsResp) +} + +func wsGetOrderbooks(client *websocketClient, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetOrderbooks", + } + wsResp.Data = getAllActiveOrderbooks(client.exchangeManager) + return client.SendWebsocketMessage(wsResp) +} + +func wsGetOrderbook(client *websocketClient, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetOrderbook", + } + var orderbookReq WebsocketOrderbookTickerRequest + err := json.Unmarshal(data.([]byte), &orderbookReq) + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(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 := client.exchangeManager.GetExchangeByName(orderbookReq.Exchange) + if exch == nil { + wsResp.Error = exchange.ErrNoExchangeFound.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(log.APIServerMgr, sendErr) + } + return err + } + ob, err := exch.FetchOrderbook(p, a) + if err != nil { + wsResp.Error = err.Error() + sendErr := client.SendWebsocketMessage(wsResp) + if sendErr != nil { + log.Error(log.APIServerMgr, sendErr) + } + return err + } + wsResp.Data = ob + return nil +} + +func wsGetExchangeRates(client *websocketClient, data interface{}) 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, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetPortfolio", + } + + wsResp.Data = client.portfolioManager.GetPortfolioSummary() + return client.SendWebsocketMessage(wsResp) +} diff --git a/engine/apiserver.md b/engine/apiserver.md new file mode 100644 index 00000000..6e71865c --- /dev/null +++ b/engine/apiserver.md @@ -0,0 +1,62 @@ +# GoCryptoTrader package Apiserver + + + + +[![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](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## 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` | + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/apiserver_test.go b/engine/apiserver_test.go new file mode 100644 index 00000000..6a6c2082 --- /dev/null +++ b/engine/apiserver_test.go @@ -0,0 +1,288 @@ +package engine + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "reflect" + "sync" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +func TestSetupAPIServerManager(t *testing.T) { + t.Parallel() + _, err := setupAPIServerManager(nil, nil, nil, nil, nil, "") + if !errors.Is(err, errNilRemoteConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilRemoteConfig) + } + + _, err = setupAPIServerManager(&config.RemoteControlConfig{}, nil, nil, nil, nil, "") + if !errors.Is(err, errNilPProfConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilPProfConfig) + } + + _, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, nil, nil, nil, "") + if !errors.Is(err, errNilExchangeManager) { + t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) + } + + _, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, nil, nil, "") + if !errors.Is(err, errNilBot) { + t.Errorf("error '%v', expected '%v'", err, errNilBot) + } + + _, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, "") + if !errors.Is(err, errEmptyConfigPath) { + t.Errorf("error '%v', expected '%v'", err, errEmptyConfigPath) + } + + wd, _ := os.Getwd() + _, err = setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestStartRESTServer(t *testing.T) { + t.Parallel() + wd, _ := os.Getwd() + m, err := setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.StartRESTServer() + if !errors.Is(err, errServerDisabled) { + t.Errorf("error '%v', expected '%v'", err, errServerDisabled) + } + m.remoteConfig.DeprecatedRPC.Enabled = true + var wg sync.WaitGroup + wg.Add(1) + // this is difficult to test as a webserver actually starts, so quit if an immediate error is not received + err = m.StartRESTServer() + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + wg.Done() +} + +func TestStartWebsocketServer(t *testing.T) { + t.Parallel() + wd, _ := os.Getwd() + m, err := setupAPIServerManager(&config.RemoteControlConfig{}, &config.Profiler{}, &ExchangeManager{}, &fakeBot{}, nil, wd) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.StartWebsocketServer() + if !errors.Is(err, errServerDisabled) { + t.Errorf("error '%v', expected '%v'", err, errServerDisabled) + } + m.remoteConfig.WebsocketRPC.Enabled = true + err = m.StartWebsocketServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +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) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.StopRESTServer() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.StartRESTServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.StopRESTServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + // do it again to ensure things have reset appropriately and no errors occur starting + err = m.StartRESTServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.StopRESTServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +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) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.StopWebsocketServer() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.StartWebsocketServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.StopWebsocketServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + // do it again to ensure things have reset appropriately and no errors occur starting + err = m.StartWebsocketServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.StopWebsocketServer() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestIsRESTServerRunning(t *testing.T) { + t.Parallel() + m := &apiServerManager{} + if m.IsRESTServerRunning() { + t.Error("expected false") + } + m.restStarted = 1 + if !m.IsRESTServerRunning() { + t.Error("expected true") + } + m = nil + if m.IsRESTServerRunning() { + t.Error("expected false") + } +} + +func TestIsWebsocketServerRunning(t *testing.T) { + t.Parallel() + m := &apiServerManager{} + if m.IsWebsocketServerRunning() { + t.Error("expected false") + } + m.websocketStarted = 1 + if !m.IsWebsocketServerRunning() { + t.Error("expected true") + } + m = nil + if m.IsWebsocketServerRunning() { + t.Error("expected false") + } +} + +func TestGetAllActiveOrderbooks(t *testing.T) { + man := SetupExchangeManager() + bs, err := man.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + bs.SetDefaults() + man.Add(bs) + resp := getAllActiveOrderbooks(man) + if resp == nil { + t.Error("expected not nil") + } +} + +func TestGetAllActiveTickers(t *testing.T) { + t.Parallel() + man := SetupExchangeManager() + bs, err := man.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + bs.SetDefaults() + man.Add(bs) + resp := getAllActiveTickers(man) + if resp == nil { + t.Error("expected not nil") + } +} + +func TestGetAllActiveAccounts(t *testing.T) { + t.Parallel() + man := SetupExchangeManager() + bs, err := man.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + bs.SetDefaults() + man.Add(bs) + resp := getAllActiveAccounts(man) + if resp == nil { + t.Error("expected not nil") + } +} + +func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response { + w := httptest.NewRecorder() + + err := writeResponse(w, response) + if err != nil { + t.Error("Failed to make response.", 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) + if err != nil { + t.Error(err) + } + resp := makeHTTPGetRequest(t, c) + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Error("Body not readable", err) + } + err = resp.Body.Close() + if err != nil { + t.Error("Body not closable", err) + } + + var responseConfig config.Config + jsonErr := json.Unmarshal(body, &responseConfig) + if jsonErr != nil { + t.Error("Response not parse-able as json", err) + } + + if !reflect.DeepEqual(responseConfig, c) { + t.Error("Json not equal to config") + } +} + +// 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 +} diff --git a/engine/apiserver_types.go b/engine/apiserver_types.go new file mode 100644 index 00000000..177f7748 --- /dev/null +++ b/engine/apiserver_types.go @@ -0,0 +1,169 @@ +package engine + +import ( + "errors" + "net/http" + "sync" + + "github.com/gorilla/mux" + "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 = "GoCryptoTrader RESTful interface. For the web GUI, please visit the web GUI readme." + 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") + errInvalidListenAddress = errors.New("invalid listen address") + 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 *websocket.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 interface{} +} + +// WebsocketEventResponse is the struct used for websocket event responses +type WebsocketEventResponse struct { + Event string `json:"event"` + Data interface{} `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.Base `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 interface{}) error +} diff --git a/engine/comms_relayer.go b/engine/comms_relayer.go deleted file mode 100644 index 049d6576..00000000 --- a/engine/comms_relayer.go +++ /dev/null @@ -1,95 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "sync/atomic" - - "github.com/thrasher-corp/gocryptotrader/communications" - "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// commsManager starts the NTP manager -type commsManager struct { - started int32 - shutdown chan struct{} - relayMsg chan base.Event - comms *communications.Communications -} - -func (c *commsManager) Started() bool { - return atomic.LoadInt32(&c.started) == 1 -} - -func (c *commsManager) Start() (err error) { - if !atomic.CompareAndSwapInt32(&c.started, 0, 1) { - return fmt.Errorf("communications manager %w", subsystem.ErrSubSystemAlreadyStarted) - } - - defer func() { - if err != nil { - atomic.CompareAndSwapInt32(&c.started, 1, 0) - } - }() - - log.Debugln(log.CommunicationMgr, "Communications manager starting...") - commsCfg := Bot.Config.GetCommunicationsConfig() - c.comms, err = communications.NewComm(&commsCfg) - if err != nil { - return err - } - - c.shutdown = make(chan struct{}) - c.relayMsg = make(chan base.Event) - go c.run() - log.Debugln(log.CommunicationMgr, "Communications manager started.") - return nil -} - -func (c *commsManager) GetStatus() (map[string]base.CommsStatus, error) { - if !c.Started() { - return nil, errors.New("communications manager not started") - } - return c.comms.GetStatus(), nil -} - -func (c *commsManager) Stop() error { - if atomic.LoadInt32(&c.started) == 0 { - return fmt.Errorf("communications manager %w", subsystem.ErrSubSystemNotStarted) - } - defer func() { - atomic.CompareAndSwapInt32(&c.started, 1, 0) - }() - close(c.shutdown) - log.Debugln(log.CommunicationMgr, "Communications manager shutting down...") - return nil -} - -func (c *commsManager) PushEvent(evt base.Event) { - if !c.Started() { - return - } - select { - case c.relayMsg <- evt: - default: - log.Errorf(log.CommunicationMgr, "Failed to send, no receiver when pushing event [%v]", evt) - } -} - -func (c *commsManager) run() { - defer func() { - // TO-DO shutdown comms connections for connected services (Slack etc) - log.Debugln(log.CommunicationMgr, "Communications manager shutdown.") - }() - - for { - select { - case msg := <-c.relayMsg: - c.comms.PushEvent(msg) - case <-c.shutdown: - return - } - } -} diff --git a/engine/communication_manager.go b/engine/communication_manager.go new file mode 100644 index 00000000..658b089f --- /dev/null +++ b/engine/communication_manager.go @@ -0,0 +1,117 @@ +package engine + +import ( + "errors" + "fmt" + "sync/atomic" + + "github.com/thrasher-corp/gocryptotrader/communications" + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// CommunicationsManagerName is an exported subsystem name +const CommunicationsManagerName = "communications" + +// CommunicationManager ensures operations of communications +type CommunicationManager struct { + started int32 + shutdown chan struct{} + relayMsg chan base.Event + comms *communications.Communications +} + +var errNilConfig = errors.New("received nil communications config") + +// SetupCommunicationManager creates a communications manager +func SetupCommunicationManager(cfg *base.CommunicationsConfig) (*CommunicationManager, error) { + if cfg == nil { + return nil, errNilConfig + } + manager := &CommunicationManager{ + shutdown: make(chan struct{}), + relayMsg: make(chan base.Event), + } + var err error + manager.comms, err = communications.NewComm(cfg) + if err != nil { + return nil, err + } + return manager, nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *CommunicationManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Start runs the subsystem +func (m *CommunicationManager) Start() error { + if m == nil { + return fmt.Errorf("communications manager server %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("communications manager %w", ErrSubSystemAlreadyStarted) + } + log.Debugf(log.CommunicationMgr, "Communications manager %s", MsgSubSystemStarting) + m.shutdown = make(chan struct{}) + go m.run() + return nil +} + +// GetStatus returns the status of communications +func (m *CommunicationManager) GetStatus() (map[string]base.CommsStatus, error) { + if !m.IsRunning() { + return nil, fmt.Errorf("communications manager %w", ErrSubSystemNotStarted) + } + return m.comms.GetStatus(), nil +} + +// Stop attempts to shutdown the subsystem +func (m *CommunicationManager) Stop() error { + if m == nil { + return fmt.Errorf("communications manager server %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("communications manager %w", ErrSubSystemNotStarted) + } + defer func() { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + }() + close(m.shutdown) + log.Debugf(log.CommunicationMgr, "Communications manager %s", MsgSubSystemShuttingDown) + return nil +} + +// PushEvent pushes an event to the communications relay +func (m *CommunicationManager) PushEvent(evt base.Event) { + if !m.IsRunning() { + return + } + select { + case m.relayMsg <- evt: + default: + log.Errorf(log.CommunicationMgr, "Failed to send, no receiver when pushing event [%v]", evt) + } +} + +// run takes awaiting messages and pushes them to be handled by communications +func (m *CommunicationManager) run() { + log.Debugf(log.Global, "Communications manager %s", MsgSubSystemStarted) + defer func() { + // TO-DO shutdown comms connections for connected services (Slack etc) + log.Debugf(log.CommunicationMgr, "Communications manager %s", MsgSubSystemShutdown) + }() + + for { + select { + case msg := <-m.relayMsg: + m.comms.PushEvent(msg) + case <-m.shutdown: + return + } + } +} diff --git a/engine/communication_manager.md b/engine/communication_manager.md new file mode 100644 index 00000000..b22ddfea --- /dev/null +++ b/engine/communication_manager.md @@ -0,0 +1,90 @@ +# GoCryptoTrader package Communication_manager + + + + +[![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/communication_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This communication_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Communication_manager ++ The communication manager subsystem is used to push events raised in GoCryptoTrader to any enabled communication system such as a Slack server ++ In order to modify the behaviour of the communication manager subsystem, you can edit the following inside your config file under `communications`: + +### slack + +| Config | Description | Example | +| ------ | ----------- | ------- | +| enabled | Determines whether the push communications to a Slack server | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| targetChannel | The channel to send communications to | `announcements` | +| verificationToken | The token generated by Slack to allow interactions with the server and channel | `iamafaketoken` | + +### smsGlobal + +| Config | Description | Example | +| ------ | ----------- | ------- | +| name | The name of the SMS sender | `SMSGlobal` | +| from | Who the text name is from | `Skynet` | +| enabled | Determines whether the push communications to the SMS service | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| username | The username to use with the SMS provider | `username` | +| password | The username to use with the SMS provider | `password` | +| contacts | The `name` `number` of the user people you wish to send SMS to and whether it is `enabled` | `"name": "StyleGherkin", "number": "1231424", "enabled": true` | + +### smtp + +| Config | Description | Example | +| ------ | ----------- | ------- | +| name | The name of the service | `SMTP` | +| enabled | Determines whether the push communications to a email server | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| host | The SMTP host | `smtp.google.com` | +| port | The port to use | `537` | +| accountName | Your username | `username` | +| accountPassword | Your password | `password` | +| from | The display name of the sender | `Jeff Bezos` | +| recipientList | A comma delimited list of addresses to send alerts to | `bill@gates.com` | + +### telegram + +| Config | Description | Example | +| ------ | ----------- | ------- | +| name | The name to be displayed | `Telegram` | +| enabled | Determines whether the push communications to a Telegram server | `true` | +| verbose | If enabled will log more details to your logger output | `false` | +| verificationToken | The token generated by Telegram to allow you to send messages | `iamafaketoken` | + + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/communication_manager_test.go b/engine/communication_manager_test.go new file mode 100644 index 00000000..4d1a3d38 --- /dev/null +++ b/engine/communication_manager_test.go @@ -0,0 +1,158 @@ +package engine + +import ( + "errors" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/communications" + "github.com/thrasher-corp/gocryptotrader/communications/base" +) + +func TestSetup(t *testing.T) { + t.Parallel() + _, err := SetupCommunicationManager(nil) + if !errors.Is(err, errNilConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilConfig) + } + + _, err = SetupCommunicationManager(&base.CommunicationsConfig{}) + if !errors.Is(err, communications.ErrNoCommunicationRelayersEnabled) { + t.Errorf("error '%v', expected '%v'", err, communications.ErrNoCommunicationRelayersEnabled) + } + + m, err := SetupCommunicationManager(&base.CommunicationsConfig{ + SlackConfig: base.SlackConfig{ + Enabled: true, + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expected manager") + } +} + +func TestIsRunning(t *testing.T) { + t.Parallel() + m, err := SetupCommunicationManager(&base.CommunicationsConfig{ + SMSGlobalConfig: base.SMSGlobalConfig{ + Enabled: true, + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if !m.IsRunning() { + t.Error("expected true") + } + m.started = 0 + if m.IsRunning() { + t.Error("expected false") + } + m = nil + if m.IsRunning() { + t.Error("expected false") + } +} + +func TestStart(t *testing.T) { + t.Parallel() + m, err := SetupCommunicationManager(&base.CommunicationsConfig{ + SMTPConfig: base.SMTPConfig{ + Enabled: true, + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.started = 1 + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } +} + +func TestGetStatus(t *testing.T) { + t.Parallel() + m, err := SetupCommunicationManager(&base.CommunicationsConfig{ + TelegramConfig: base.TelegramConfig{ + Enabled: true, + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + _, err = m.GetStatus() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.started = 0 + _, err = m.GetStatus() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } +} + +func TestStop(t *testing.T) { + t.Parallel() + m, err := SetupCommunicationManager(&base.CommunicationsConfig{ + SlackConfig: base.SlackConfig{ + Enabled: true, + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + m = nil + err = m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestPushEvent(t *testing.T) { + t.Parallel() + m, err := SetupCommunicationManager(&base.CommunicationsConfig{ + SlackConfig: base.SlackConfig{ + Enabled: true, + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.PushEvent(base.Event{}) + time.Sleep(time.Second) + m.PushEvent(base.Event{}) + m = nil + m.PushEvent(base.Event{}) +} diff --git a/engine/connection.go b/engine/connection.go deleted file mode 100644 index a4601917..00000000 --- a/engine/connection.go +++ /dev/null @@ -1,67 +0,0 @@ -package engine - -import ( - "fmt" - "sync/atomic" - - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/connchecker" - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// connectionManager manages the connchecker -type connectionManager struct { - started int32 - conn *connchecker.Checker -} - -// Started returns if the connection manager has started -func (c *connectionManager) Started() bool { - return atomic.LoadInt32(&c.started) == 1 -} - -// Start starts an instance of the connection manager -func (c *connectionManager) Start(conf *config.ConnectionMonitorConfig) error { - if !atomic.CompareAndSwapInt32(&c.started, 0, 1) { - return fmt.Errorf("connection manager %w", subsystem.ErrSubSystemAlreadyStarted) - } - - log.Debugln(log.ConnectionMgr, "Connection manager starting...") - var err error - c.conn, err = connchecker.New(conf.DNSList, - conf.PublicDomainList, - conf.CheckInterval) - if err != nil { - atomic.CompareAndSwapInt32(&c.started, 1, 0) - return err - } - - log.Debugln(log.ConnectionMgr, "Connection manager started.") - return nil -} - -// Stop stops the connection manager -func (c *connectionManager) Stop() error { - if atomic.LoadInt32(&c.started) == 0 { - return fmt.Errorf("connection manager %w", subsystem.ErrSubSystemNotStarted) - } - defer func() { - atomic.CompareAndSwapInt32(&c.started, 1, 0) - }() - - log.Debugln(log.ConnectionMgr, "Connection manager shutting down...") - c.conn.Shutdown() - log.Debugln(log.ConnectionMgr, "Connection manager stopped.") - return nil -} - -// IsOnline returns if the connection manager is online -func (c *connectionManager) IsOnline() bool { - if c.conn == nil { - log.Warnln(log.ConnectionMgr, "Connection manager: IsOnline called but conn is nil") - return false - } - - return c.conn.IsConnected() -} diff --git a/engine/connection_manager.go b/engine/connection_manager.go new file mode 100644 index 00000000..cf8b7ed8 --- /dev/null +++ b/engine/connection_manager.go @@ -0,0 +1,100 @@ +package engine + +import ( + "fmt" + "sync/atomic" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/connchecker" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// ConnectionManagerName is an exported subsystem name +const ConnectionManagerName = "internet_monitor" + +// connectionManager manages the connchecker +type connectionManager struct { + started int32 + conn *connchecker.Checker + cfg *config.ConnectionMonitorConfig +} + +// IsRunning safely checks whether the subsystem is running +func (m *connectionManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// setupConnectionManager creates a connection manager +func setupConnectionManager(cfg *config.ConnectionMonitorConfig) (*connectionManager, error) { + if cfg == nil { + return nil, errNilConfig + } + if cfg.DNSList == nil { + cfg.DNSList = connchecker.DefaultDNSList + } + if cfg.PublicDomainList == nil { + cfg.PublicDomainList = connchecker.DefaultDomainList + } + if cfg.CheckInterval == 0 { + cfg.CheckInterval = connchecker.DefaultCheckInterval + } + return &connectionManager{ + cfg: cfg, + }, nil +} + +// Start runs the subsystem +func (m *connectionManager) Start() error { + if m == nil { + return fmt.Errorf("connection manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("connection manager %w", ErrSubSystemAlreadyStarted) + } + + log.Debugln(log.ConnectionMgr, "Connection manager starting...") + var err error + m.conn, err = connchecker.New(m.cfg.DNSList, + m.cfg.PublicDomainList, + m.cfg.CheckInterval) + if err != nil { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + return err + } + + log.Debugln(log.ConnectionMgr, "Connection manager started.") + return nil +} + +// Stop stops the connection manager +func (m *connectionManager) Stop() error { + if m == nil { + return fmt.Errorf("connection manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("connection manager %w", ErrSubSystemNotStarted) + } + defer func() { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + }() + log.Debugln(log.ConnectionMgr, "Connection manager shutting down...") + m.conn.Shutdown() + log.Debugln(log.ConnectionMgr, "Connection manager stopped.") + return nil +} + +// IsOnline returns if the connection manager is online +func (m *connectionManager) IsOnline() bool { + if m == nil { + return false + } + if m.conn == nil { + log.Warnln(log.ConnectionMgr, "Connection manager: IsOnline called but conn is nil") + return false + } + + return m.conn.IsConnected() +} diff --git a/engine/connection_manager.md b/engine/connection_manager.md new file mode 100644 index 00000000..da6d260c --- /dev/null +++ b/engine/connection_manager.md @@ -0,0 +1,53 @@ +# GoCryptoTrader package Connection_manager + + + + +[![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/connection_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This connection_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Connection_manager ++ The connection manager subsystem is used to periodically check whether the application is connected to the internet and will provide alerts of any changes ++ In order to modify the behaviour of the connection manager subsystem, you can edit the following inside your config file under `connectionMonitor`: + +### connectionMonitor + +| Config | Description | Example | +| ------ | ----------- | ------- | +| perferredDNSList | Is a string array of DNS servers to periodically verify whether GoCryptoTrader is connected to the internet | `["8.8.8.8","8.8.4.4","1.1.1.1","1.0.0.1"]` | +| preferredDomainList | Is a string array of domains to periodically verify whether GoCryptoTrader is connected to the internet | `["www.google.com","www.cloudflare.com","www.facebook.com"]` | +| checkInterval | A time period in golang `time.Duration` format to check whether GoCryptoTrader is connected to the internet | `1000000000` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/connection_manager_test.go b/engine/connection_manager_test.go new file mode 100644 index 00000000..a66b054d --- /dev/null +++ b/engine/connection_manager_test.go @@ -0,0 +1,122 @@ +package engine + +import ( + "errors" + "testing" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +func TestSetupConnectionManager(t *testing.T) { + t.Parallel() + _, err := setupConnectionManager(nil) + if !errors.Is(err, errNilConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilConfig) + } + + m, err := setupConnectionManager(&config.ConnectionMonitorConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expected manager") + } +} + +func TestConnectionMonitorIsRunning(t *testing.T) { + t.Parallel() + m, err := setupConnectionManager(&config.ConnectionMonitorConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if !m.IsRunning() { + t.Error("expected true") + } + m.started = 0 + if m.IsRunning() { + t.Error("expected false") + } + m = nil + if m.IsRunning() { + t.Error("expected false") + } +} + +func TestConnectionMonitorStart(t *testing.T) { + t.Parallel() + m, err := setupConnectionManager(&config.ConnectionMonitorConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } + m = nil + err = m.Start() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestConnectionMonitorStop(t *testing.T) { + t.Parallel() + m, err := setupConnectionManager(&config.ConnectionMonitorConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + m = nil + err = m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestConnectionMonitorIsOnline(t *testing.T) { + t.Parallel() + m, err := setupConnectionManager(&config.ConnectionMonitorConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + // If someone runs this offline, who are we to fail them? + m.IsOnline() + err = m.Stop() + if err != nil { + t.Fatal(err) + } + if m.IsOnline() { + t.Error("expected false") + } + m.conn = nil + if m.IsOnline() { + t.Error("expected false") + } + m = nil + if m.IsOnline() { + t.Error("expected false") + } +} diff --git a/engine/database.go b/engine/database.go deleted file mode 100644 index 1443ff5d..00000000 --- a/engine/database.go +++ /dev/null @@ -1,132 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "sync/atomic" - "time" - - "github.com/thrasher-corp/gocryptotrader/database" - dbpsql "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres" - dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3" - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" - "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/sqlboiler/boil" -) - -var ( - dbConn *database.Instance -) - -type databaseManager struct { - started int32 - shutdown chan struct{} -} - -func (a *databaseManager) Started() bool { - return atomic.LoadInt32(&a.started) == 1 -} - -func (a *databaseManager) Start(bot *Engine) (err error) { - if !atomic.CompareAndSwapInt32(&a.started, 0, 1) { - return fmt.Errorf("database manager %w", subsystem.ErrSubSystemAlreadyStarted) - } - - defer func() { - if err != nil { - atomic.CompareAndSwapInt32(&a.started, 1, 0) - } - }() - - log.Debugln(log.DatabaseMgr, "Database manager starting...") - - a.shutdown = make(chan struct{}) - - if bot.Config.Database.Enabled { - if bot.Config.Database.Driver == database.DBPostgreSQL { - log.Debugf(log.DatabaseMgr, - "Attempting to establish database connection to host %s/%s utilising %s driver\n", - bot.Config.Database.Host, - bot.Config.Database.Database, - bot.Config.Database.Driver) - dbConn, err = dbpsql.Connect() - } else if bot.Config.Database.Driver == database.DBSQLite || - bot.Config.Database.Driver == database.DBSQLite3 { - log.Debugf(log.DatabaseMgr, - "Attempting to establish database connection to %s utilising %s driver\n", - bot.Config.Database.Database, - bot.Config.Database.Driver) - dbConn, err = dbsqlite3.Connect() - } - if err != nil { - return fmt.Errorf("database failed to connect: %v Some features that utilise a database will be unavailable", err) - } - dbConn.Connected = true - - DBLogger := database.Logger{} - if bot.Config.Database.Verbose { - boil.DebugMode = true - boil.DebugWriter = DBLogger - } - - go a.run(bot) - return nil - } - - return errors.New("database support disabled") -} - -func (a *databaseManager) Stop() error { - if atomic.LoadInt32(&a.started) == 0 { - return fmt.Errorf("database manager %w", subsystem.ErrSubSystemNotStarted) - } - defer func() { - atomic.CompareAndSwapInt32(&a.started, 1, 0) - }() - - err := dbConn.SQL.Close() - if err != nil { - log.Errorf(log.DatabaseMgr, "Failed to close database: %v", err) - } - - close(a.shutdown) - return nil -} - -func (a *databaseManager) run(bot *Engine) { - log.Debugln(log.DatabaseMgr, "Database manager started.") - bot.ServicesWG.Add(1) - t := time.NewTicker(time.Second * 2) - - defer func() { - t.Stop() - bot.ServicesWG.Done() - log.Debugln(log.DatabaseMgr, "Database manager shutdown.") - }() - - for { - select { - case <-a.shutdown: - return - case <-t.C: - go a.checkConnection() - } - } -} - -func (a *databaseManager) checkConnection() { - dbConn.Mu.Lock() - defer dbConn.Mu.Unlock() - - err := dbConn.SQL.Ping() - if err != nil { - log.Errorf(log.DatabaseMgr, "Database connection error: %v\n", err) - dbConn.Connected = false - return - } - - if !dbConn.Connected { - log.Info(log.DatabaseMgr, "Database connection reestablished") - dbConn.Connected = true - } -} diff --git a/engine/database_connection.go b/engine/database_connection.go new file mode 100644 index 00000000..a06e1c3f --- /dev/null +++ b/engine/database_connection.go @@ -0,0 +1,191 @@ +package engine + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/database" + dbpsql "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres" + dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// DatabaseConnectionManagerName is an exported subsystem name +const DatabaseConnectionManagerName = "database" + +var ( + errDatabaseDisabled = errors.New("database support disabled") +) + +// DatabaseConnectionManager holds the database connection and its status +type DatabaseConnectionManager struct { + started int32 + shutdown chan struct{} + enabled bool + verbose bool + host string + username string + password string + database string + driver string + wg sync.WaitGroup + dbConn *database.Instance +} + +// IsRunning safely checks whether the subsystem is running +func (m *DatabaseConnectionManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// SetupDatabaseConnectionManager creates a new database manager +func SetupDatabaseConnectionManager(cfg *database.Config) (*DatabaseConnectionManager, error) { + if cfg == nil { + return nil, errNilConfig + } + m := &DatabaseConnectionManager{ + shutdown: make(chan struct{}), + enabled: cfg.Enabled, + verbose: cfg.Verbose, + host: cfg.Host, + username: cfg.Username, + password: cfg.Password, + database: cfg.Database, + driver: cfg.Driver, + dbConn: database.DB, + } + err := m.dbConn.SetConfig(cfg) + if err != nil { + return nil, err + } + + return m, nil +} + +// Start sets up the database connection manager to maintain a SQL connection +func (m *DatabaseConnectionManager) Start(wg *sync.WaitGroup) (err error) { + if m == nil { + return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("database manager %w", ErrSubSystemAlreadyStarted) + } + defer func() { + if err != nil { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + } + }() + + log.Debugln(log.DatabaseMgr, "Database manager starting...") + + if m.enabled { + m.shutdown = make(chan struct{}) + switch m.driver { + case database.DBPostgreSQL: + log.Debugf(log.DatabaseMgr, + "Attempting to establish database connection to host %s/%s utilising %s driver\n", + m.host, + m.database, + m.driver) + m.dbConn, err = dbpsql.Connect() + case database.DBSQLite, + database.DBSQLite3: + log.Debugf(log.DatabaseMgr, + "Attempting to establish database connection to %s utilising %s driver\n", + m.database, + m.driver) + m.dbConn, err = dbsqlite3.Connect() + default: + return database.ErrNoDatabaseProvided + } + if err != nil { + return fmt.Errorf("%w: %v Some features that utilise a database will be unavailable", database.ErrFailedToConnect, err) + } + m.dbConn.SetConnected(true) + wg.Add(1) + m.wg.Add(1) + go m.run(wg) + return nil + } + + return errDatabaseDisabled +} + +// Stop stops the database manager and closes the connection +// Stop attempts to shutdown the subsystem +func (m *DatabaseConnectionManager) Stop() error { + if m == nil { + return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrSubSystemNotStarted) + } + defer func() { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + }() + + err := m.dbConn.CloseConnection() + if err != nil { + log.Errorf(log.DatabaseMgr, "Failed to close database: %v", err) + } + + close(m.shutdown) + m.wg.Wait() + return nil +} + +func (m *DatabaseConnectionManager) run(wg *sync.WaitGroup) { + log.Debugln(log.DatabaseMgr, "Database manager started.") + t := time.NewTicker(time.Second * 2) + + defer func() { + t.Stop() + m.wg.Done() + wg.Done() + log.Debugln(log.DatabaseMgr, "Database manager shutdown.") + }() + + for { + select { + case <-m.shutdown: + return + case <-t.C: + err := m.checkConnection() + if err != nil { + log.Error(log.DatabaseMgr, "Database connection error:", err) + } + } + } +} + +func (m *DatabaseConnectionManager) checkConnection() error { + if m == nil { + return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrSubSystemNotStarted) + } + if !m.enabled { + return database.ErrDatabaseSupportDisabled + } + if m.dbConn == nil { + return database.ErrNoDatabaseProvided + } + + err := m.dbConn.Ping() + if err != nil { + m.dbConn.SetConnected(false) + return err + } + + if !m.dbConn.IsConnected() { + log.Info(log.DatabaseMgr, "Database connection reestablished") + m.dbConn.SetConnected(true) + } + return nil +} diff --git a/engine/database_connection.md b/engine/database_connection.md new file mode 100644 index 00000000..bd47a2f7 --- /dev/null +++ b/engine/database_connection.md @@ -0,0 +1,64 @@ +# GoCryptoTrader package Database_connection + + + + +[![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/database_connection) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This database_connection 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Database_connection ++ The database connection manager subsystem is used to periodically check whether the application is connected to the database and will provide alerts of any changes ++ In order to modify the behaviour of the database connection manager subsystem, you can edit the following inside your config file under `database`: + +### database + +| Config | Description | Example | +| ------ | ----------- | ------- | +| enabled | Enabled or disables the database connection subsystem | `true` | +| verbose | Displays more information to the logger which can be helpful for debugging | `false` | +| driver | The SQL driver to use. Can be `postgres` or `sqlite`. | `sqlite` | +| connectionDetails | See below | | + +### connectionDetails + +| Config | Description | Example | +| ------ | ----------- | ------- | +| host | The host address of the database | `localhost` | +| port | The port used to connect to the database | `5432` | +| username | An optional username to connect to the database | `username` | +| password | An optional password to connect to the database | `password` | +| database | The name of the database | `database.db` | +| sslmode | The connection type of the database for Postgres databases only | `disable` | + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/database_connection_test.go b/engine/database_connection_test.go new file mode 100644 index 00000000..72c2d5e1 --- /dev/null +++ b/engine/database_connection_test.go @@ -0,0 +1,241 @@ +package engine + +import ( + "errors" + "io/ioutil" + "log" + "os" + "sync" + "testing" + + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/database/drivers" +) + +func CreateDatabase(t *testing.T) string { + t.Helper() + // fun workarounds to globals ruining testing + tmpDir, err := ioutil.TempDir("", "") + if err != nil { + log.Fatal(err) + } + database.DB.DataPath = tmpDir + return tmpDir +} + +func Cleanup(t *testing.T, tmpDir string) { + if database.DB.IsConnected() { + err := database.DB.CloseConnection() + if err != nil { + log.Fatal(err) + } + err = os.RemoveAll(tmpDir) + if err != nil { + log.Fatal(err) + } + } +} + +func TestSetupDatabaseConnectionManager(t *testing.T) { + _, err := SetupDatabaseConnectionManager(nil) + if !errors.Is(err, errNilConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilConfig) + } + + m, err := SetupDatabaseConnectionManager(&database.Config{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expected manager") + } +} + +func TestStartSQLite(t *testing.T) { + tmpDir := CreateDatabase(t) + defer Cleanup(t, tmpDir) + m, err := SetupDatabaseConnectionManager(&database.Config{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + var wg sync.WaitGroup + err = m.Start(&wg) + if !errors.Is(err, errDatabaseDisabled) { + t.Errorf("error '%v', expected '%v'", err, errDatabaseDisabled) + } + m, err = SetupDatabaseConnectionManager(&database.Config{Enabled: true}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start(&wg) + if !errors.Is(err, database.ErrNoDatabaseProvided) { + t.Errorf("error '%v', expected '%v'", err, database.ErrNoDatabaseProvided) + } + m.driver = database.DBSQLite + err = m.Start(&wg) + if !errors.Is(err, database.ErrFailedToConnect) { + t.Errorf("error '%v', expected '%v'", err, database.ErrFailedToConnect) + } + _, err = SetupDatabaseConnectionManager(&database.Config{ + Enabled: true, + Driver: database.DBSQLite, + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + Database: "test.db", + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +// This test does not care for a successful connection +func TestStartPostgres(t *testing.T) { + tmpDir := CreateDatabase(t) + defer Cleanup(t, tmpDir) + m, err := SetupDatabaseConnectionManager(&database.Config{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + var wg sync.WaitGroup + err = m.Start(&wg) + if !errors.Is(err, errDatabaseDisabled) { + t.Errorf("error '%v', expected '%v'", err, errDatabaseDisabled) + } + m.enabled = true + err = m.Start(&wg) + if !errors.Is(err, database.ErrNoDatabaseProvided) { + t.Errorf("error '%v', expected '%v'", err, database.ErrNoDatabaseProvided) + } + m.driver = database.DBPostgreSQL + err = m.Start(&wg) + if !errors.Is(err, database.ErrFailedToConnect) { + t.Errorf("error '%v', expected '%v'", err, database.ErrFailedToConnect) + } +} + +func TestDatabaseConnectionManagerIsRunning(t *testing.T) { + tmpDir := CreateDatabase(t) + defer Cleanup(t, tmpDir) + m, err := SetupDatabaseConnectionManager(&database.Config{ + Enabled: true, + Driver: database.DBSQLite, + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + Database: "test.db", + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.IsRunning() { + t.Error("expected false") + } + var wg sync.WaitGroup + err = m.Start(&wg) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if !m.IsRunning() { + t.Error("expected true") + } + m = nil + if m.IsRunning() { + t.Error("expected false") + } +} + +func TestDatabaseConnectionManagerStop(t *testing.T) { + tmpDir := CreateDatabase(t) + defer Cleanup(t, tmpDir) + m, err := SetupDatabaseConnectionManager(&database.Config{ + Enabled: true, + Driver: database.DBSQLite, + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + Database: "test.db", + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + var wg sync.WaitGroup + err = m.Start(&wg) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + m = nil + err = m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestCheckConnection(t *testing.T) { + tmpDir := CreateDatabase(t) + defer Cleanup(t, tmpDir) + var m *DatabaseConnectionManager + err := m.checkConnection() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + m, err = SetupDatabaseConnectionManager(&database.Config{ + Enabled: true, + Driver: database.DBSQLite, + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + Database: "test.db", + }, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.checkConnection() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + var wg sync.WaitGroup + err = m.Start(&wg) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.checkConnection() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.checkConnection() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start(&wg) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.checkConnection() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + m.dbConn.SetConnected(false) + err = m.checkConnection() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} diff --git a/engine/depositaddress.go b/engine/depositaddress.go new file mode 100644 index 00000000..c88913ad --- /dev/null +++ b/engine/depositaddress.go @@ -0,0 +1,91 @@ +package engine + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// vars related to the deposit address helpers +var ( + ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil") + ErrDepositAddressNotFound = errors.New("deposit address does not exist") +) + +// DepositAddressManager manages the exchange deposit address store +type DepositAddressManager struct { + m sync.Mutex + store map[string]map[string]string +} + +// SetupDepositAddressManager returns a DepositAddressManager +func SetupDepositAddressManager() *DepositAddressManager { + return &DepositAddressManager{ + store: make(map[string]map[string]string), + } +} + +// GetDepositAddressByExchangeAndCurrency returns a deposit address for the specified exchange and cryptocurrency +// if it exists +func (m *DepositAddressManager) GetDepositAddressByExchangeAndCurrency(exchName string, currencyItem currency.Code) (string, error) { + m.m.Lock() + defer m.m.Unlock() + + if len(m.store) == 0 { + return "", ErrDepositAddressStoreIsNil + } + + r, ok := m.store[strings.ToUpper(exchName)] + if !ok { + return "", ErrExchangeNotFound + } + + addr, ok := r[strings.ToUpper(currencyItem.String())] + if !ok { + return "", ErrDepositAddressNotFound + } + + return addr, nil +} + +// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified +// exchange if they exist +func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string]string, error) { + m.m.Lock() + defer m.m.Unlock() + + if len(m.store) == 0 { + return nil, ErrDepositAddressStoreIsNil + } + + r, ok := m.store[strings.ToUpper(exchName)] + if !ok { + return nil, ErrDepositAddressNotFound + } + + return r, nil +} + +// Sync synchronises all deposit addresses +func (m *DepositAddressManager) Sync(addresses map[string]map[string]string) error { + if m == nil { + return fmt.Errorf("deposit address manager %w", ErrNilSubsystem) + } + m.m.Lock() + defer m.m.Unlock() + if m.store == nil { + return ErrDepositAddressStoreIsNil + } + + for k, v := range addresses { + r := make(map[string]string) + for w, x := range v { + r[strings.ToUpper(w)] = x + } + m.store[strings.ToUpper(k)] = r + } + return nil +} diff --git a/engine/depositaddress.md b/engine/depositaddress.md new file mode 100644 index 00000000..7d00ec0b --- /dev/null +++ b/engine/depositaddress.md @@ -0,0 +1,45 @@ +# GoCryptoTrader package Depositaddress + + + + +[![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/depositaddress) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This depositaddress 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Depositaddress ++ The deposit address manager subsystem stores Exchange deposit addresses. ++ On start of the application the engine Bot will retrieve deposit addresses from exchanges if you have API keys set + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/depositaddress_test.go b/engine/depositaddress_test.go new file mode 100644 index 00000000..a9a6767a --- /dev/null +++ b/engine/depositaddress_test.go @@ -0,0 +1,96 @@ +package engine + +import ( + "errors" + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +const ( + address = "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" + bitStamp = "BITSTAMP" + btc = "BTC" +) + +func TestSetupDepositAddressManager(t *testing.T) { + m := SetupDepositAddressManager() + if m.store == nil { + t.Fatal("expected store") + } +} + +func TestSync(t *testing.T) { + m := SetupDepositAddressManager() + err := m.Sync(map[string]map[string]string{ + bitStamp: { + btc: address, + }, + }) + if err != nil { + t.Error(err) + } + r, err := m.GetDepositAddressByExchangeAndCurrency(bitStamp, currency.BTC) + if err != nil { + t.Error("unexpected result") + } + if r != address { + t.Error("unexpected result") + } + + m.store = nil + err = m.Sync(map[string]map[string]string{ + bitStamp: { + btc: address, + }, + }) + if !errors.Is(err, ErrDepositAddressStoreIsNil) { + t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil) + } + + m = nil + err = m.Sync(map[string]map[string]string{ + bitStamp: { + btc: address, + }, + }) + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("received %v, expected %v", err, ErrNilSubsystem) + } +} + +func TestGetDepositAddressByExchangeAndCurrency(t *testing.T) { + m := SetupDepositAddressManager() + _, err := m.GetDepositAddressByExchangeAndCurrency("", currency.BTC) + if !errors.Is(err, ErrDepositAddressStoreIsNil) { + t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil) + } + + m.store = map[string]map[string]string{ + bitStamp: { + btc: address, + }, + } + _, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, currency.BTC) + if !errors.Is(err, nil) { + t.Errorf("received %v, expected %v", err, nil) + } +} + +func TestGetDepositAddressesByExchange(t *testing.T) { + m := SetupDepositAddressManager() + _, err := m.GetDepositAddressesByExchange("") + if !errors.Is(err, ErrDepositAddressStoreIsNil) { + t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil) + } + + m.store = map[string]map[string]string{ + bitStamp: { + btc: address, + }, + } + _, err = m.GetDepositAddressesByExchange(bitStamp) + if !errors.Is(err, nil) { + t.Errorf("received %v, expected %v", err, nil) + } +} diff --git a/engine/engine.go b/engine/engine.go index e32935cc..a1686914 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "os" "path/filepath" "runtime" "strings" @@ -15,39 +16,42 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap" "github.com/thrasher-corp/gocryptotrader/dispatch" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" gctscript "github.com/thrasher-corp/gocryptotrader/gctscript/vm" gctlog "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/portfolio" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" "github.com/thrasher-corp/gocryptotrader/utils" ) -// Engine contains configuration, portfolio, exchange & ticker data and is the +// Engine contains configuration, portfolio manager, exchange & ticker data and is the // overarching type across this code base. type Engine struct { - Config *config.Config - Portfolio *portfolio.Base - ExchangeCurrencyPairManager *ExchangeCurrencyPairSyncer - NTPManager ntpManager - ConnectionManager connectionManager - DatabaseManager databaseManager - GctScriptManager *gctscript.GctScriptManager - OrderManager orderManager - PortfolioManager portfolioManager - CommsManager commsManager - exchangeManager exchangeManager - DepositAddressManager *DepositAddressManager - Settings Settings - Uptime time.Time - ServicesWG sync.WaitGroup + Config *config.Config + apiServer *apiServerManager + CommunicationsManager *CommunicationManager + connectionManager *connectionManager + currencyPairSyncer *syncManager + DatabaseManager *DatabaseConnectionManager + DepositAddressManager *DepositAddressManager + eventManager *eventManager + ExchangeManager *ExchangeManager + ntpManager *ntpManager + OrderManager *OrderManager + portfolioManager *portfolioManager + gctScriptManager *gctscript.GctScriptManager + websocketRoutineManager *websocketRoutineManager + WithdrawManager *WithdrawManager + Settings Settings + uptime time.Time + ServicesWG sync.WaitGroup } -// Vars for engine -var ( - Bot *Engine -) +// Bot is a happy global engine to allow various areas of the application +// to access its setup services and functions +var Bot *Engine // New starts a new engine func New() (*Engine, error) { @@ -60,10 +64,6 @@ func New() (*Engine, error) { if err != nil { return nil, fmt.Errorf("failed to load config. Err: %s", err) } - b.GctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript) - if err != nil { - return nil, fmt.Errorf("failed to create script manager. Err: %s", err) - } return &b, nil } @@ -99,7 +99,7 @@ func NewFromSettings(settings *Settings, flagSet map[string]bool) (*Engine, erro return nil, fmt.Errorf("unable to adjust runtime GOMAXPROCS value. Err: %s", err) } - b.GctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript) + b.gctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript) if err != nil { return nil, fmt.Errorf("failed to create script manager. Err: %s", err) } @@ -182,7 +182,7 @@ func validateSettings(b *Engine, s *Settings, flagSet map[string]bool) { if flagSet["maxvirtualmachines"] { maxMachines := uint8(s.MaxVirtualMachines) - b.GctScriptManager.MaxVirtualMachines = &maxMachines + b.gctScriptManager.MaxVirtualMachines = &maxMachines } if flagSet["withdrawcachesize"] { @@ -344,36 +344,57 @@ func (bot *Engine) Start() error { if bot == nil { return errors.New("engine instance is nil") } - + var err error newEngineMutex.Lock() defer newEngineMutex.Unlock() if bot.Settings.EnableDatabaseManager { - if err := bot.DatabaseManager.Start(bot); err != nil { - gctlog.Errorf(gctlog.Global, "Database manager unable to start: %v", err) + bot.DatabaseManager, err = SetupDatabaseConnectionManager(&bot.Config.Database) + if err != nil { + gctlog.Errorf(gctlog.Global, "Database manager unable to setup: %v", err) + } else { + err = bot.DatabaseManager.Start(&bot.ServicesWG) + if err != nil { + gctlog.Errorf(gctlog.Global, "Database manager unable to start: %v", err) + } } } if bot.Settings.EnableDispatcher { - if err := dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit); err != nil { + if err = dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit); err != nil { gctlog.Errorf(gctlog.DispatchMgr, "Dispatcher unable to start: %v", err) } } // Sets up internet connectivity monitor if bot.Settings.EnableConnectivityMonitor { - if err := bot.ConnectionManager.Start(&bot.Config.ConnectionMonitor); err != nil { - gctlog.Errorf(gctlog.Global, "Connection manager unable to start: %v", err) + bot.connectionManager, err = setupConnectionManager(&bot.Config.ConnectionMonitor) + if err != nil { + gctlog.Errorf(gctlog.Global, "Connection manager unable to setup: %v", err) + } else { + err = bot.connectionManager.Start() + if err != nil { + gctlog.Errorf(gctlog.Global, "Connection manager unable to start: %v", err) + } } } if bot.Settings.EnableNTPClient { - if err := bot.NTPManager.Start(); err != nil { - gctlog.Errorf(gctlog.Global, "NTP manager unable to start: %v", err) + if bot.Config.NTPClient.Level == 0 { + var responseMessage string + responseMessage, err = bot.Config.SetNTPCheck(os.Stdin) + if err != nil { + return fmt.Errorf("unable to set NTP check: %w", err) + } + gctlog.Info(gctlog.TimeMgr, responseMessage) + } + bot.ntpManager, err = setupNTPManager(&bot.Config.NTPClient, *bot.Config.Logging.Enabled) + if err != nil { + gctlog.Errorf(gctlog.Global, "NTP manager unable to start: %s", err) } } - bot.Uptime = time.Now() + bot.uptime = time.Now() gctlog.Debugf(gctlog.Global, "Bot '%s' started.\n", bot.Config.Name) gctlog.Debugf(gctlog.Global, "Using data dir: %s\n", bot.Settings.DataDir) if *bot.Config.Logging.Enabled && strings.Contains(bot.Config.Logging.Output, "file") { @@ -398,15 +419,22 @@ func (bot *Engine) Start() error { bot.Config.PurgeExchangeAPICredentials() } + bot.ExchangeManager = SetupExchangeManager() gctlog.Debugln(gctlog.Global, "Setting up exchanges..") - err := bot.SetupExchanges() + err = bot.SetupExchanges() if err != nil { return err } if bot.Settings.EnableCommsRelayer { - if err = bot.CommsManager.Start(); err != nil { - gctlog.Errorf(gctlog.Global, "Communications manager unable to start: %v\n", err) + bot.CommunicationsManager, err = SetupCommunicationManager(&bot.Config.Communications) + if err != nil { + gctlog.Errorf(gctlog.Global, "Communications manager unable to setup: %s", err) + } else { + err = bot.CommunicationsManager.Start() + if err != nil { + gctlog.Errorf(gctlog.Global, "Communications manager unable to start: %s", err) + } } } if bot.Settings.EnableCoinmarketcapAnalysis || @@ -433,7 +461,7 @@ func (bot *Engine) Start() error { }, bot.Settings.DataDir) if err != nil { - gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %v", err) + gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %s", err) } } @@ -441,34 +469,79 @@ func (bot *Engine) Start() error { go StartRPCServer(bot) } - if bot.Settings.EnableDeprecatedRPC { - go StartRESTServer(bot) - } - - if bot.Settings.EnableWebsocketRPC { - go StartWebsocketServer(bot) - StartWebsocketHandler() - } - if bot.Settings.EnablePortfolioManager { - if err = bot.PortfolioManager.Start(); err != nil { - gctlog.Errorf(gctlog.Global, "Fund manager unable to start: %v", err) + if bot.portfolioManager == nil { + bot.portfolioManager, err = setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, &bot.Config.Portfolio) + if err != nil { + gctlog.Errorf(gctlog.Global, "portfolio manager unable to setup: %s", err) + } else { + err = bot.portfolioManager.Start(&bot.ServicesWG) + if err != nil { + gctlog.Errorf(gctlog.Global, "portfolio manager unable to start: %s", err) + } + } + } + } + + bot.WithdrawManager, err = SetupWithdrawManager(bot.ExchangeManager, bot.portfolioManager, bot.Settings.EnableDryRun) + if err != nil { + return err + } + + if bot.Settings.EnableDeprecatedRPC || + bot.Settings.EnableWebsocketRPC { + 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 { + gctlog.Errorf(gctlog.Global, "API Server unable to start: %s", err) + } else { + if bot.Settings.EnableDeprecatedRPC { + err = bot.apiServer.StartRESTServer() + if err != nil { + gctlog.Errorf(gctlog.Global, "could not start REST API server: %s", err) + } + } + if bot.Settings.EnableWebsocketRPC { + err = bot.apiServer.StartWebsocketServer() + if err != nil { + gctlog.Errorf(gctlog.Global, "could not start websocket API server: %s", err) + } + } } } if bot.Settings.EnableDepositAddressManager { - bot.DepositAddressManager = new(DepositAddressManager) - go bot.DepositAddressManager.Sync() + bot.DepositAddressManager = SetupDepositAddressManager() + go func() { + err = bot.DepositAddressManager.Sync(bot.GetExchangeCryptocurrencyDepositAddresses()) + if err != nil { + gctlog.Errorf(gctlog.Global, "Deposit address manager unable to setup: %s", err) + } + }() } if bot.Settings.EnableOrderManager { - if err = bot.OrderManager.Start(bot); err != nil { - gctlog.Errorf(gctlog.Global, "Order manager unable to start: %v", err) + bot.OrderManager, err = SetupOrderManager( + bot.ExchangeManager, + bot.CommunicationsManager, + &bot.ServicesWG, + bot.Settings.Verbose) + if err != nil { + gctlog.Errorf(gctlog.Global, "Order manager unable to setup: %s", err) + } else { + err = bot.OrderManager.Start() + if err != nil { + gctlog.Errorf(gctlog.Global, "Order manager unable to start: %s", err) + } } } if bot.Settings.EnableExchangeSyncManager { - exchangeSyncCfg := CurrencyPairSyncerConfig{ + exchangeSyncCfg := &Config{ SyncTicker: bot.Settings.EnableTickerSyncing, SyncOrderbook: bot.Settings.EnableOrderbookSyncing, SyncTrades: bot.Settings.EnableTradeSyncing, @@ -479,25 +552,54 @@ func (bot *Engine) Start() error { SyncTimeoutWebsocket: bot.Settings.SyncTimeoutWebsocket, } - bot.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(exchangeSyncCfg) + bot.currencyPairSyncer, err = setupSyncManager( + exchangeSyncCfg, + bot.ExchangeManager, + bot.websocketRoutineManager, + &bot.Config.RemoteControl) if err != nil { - gctlog.Warnf(gctlog.Global, "Unable to initialise exchange currency pair syncer. Err: %s", err) + gctlog.Errorf(gctlog.Global, "Unable to initialise exchange currency pair syncer. Err: %s", err) } else { - go bot.ExchangeCurrencyPairManager.Start() + go func() { + err = bot.currencyPairSyncer.Start() + if err != nil { + gctlog.Errorf(gctlog.Global, "failed to start exchange currency pair manager. Err: %s", err) + } + }() } } if bot.Settings.EnableEventManager { - go EventManger(bot.Settings.Verbose, &bot.CommsManager) + bot.eventManager, err = setupEventManager(bot.CommunicationsManager, bot.ExchangeManager, bot.Settings.EventManagerDelay, bot.Settings.EnableDryRun) + if err != nil { + gctlog.Errorf(gctlog.Global, "Unable to initialise event manager. Err: %s", err) + } else { + err = bot.eventManager.Start() + if err != nil { + gctlog.Errorf(gctlog.Global, "failed to start event manager. Err: %s", err) + } + } } if bot.Settings.EnableWebsocketRoutine { - go bot.WebsocketRoutine() + bot.websocketRoutineManager, err = setupWebsocketRoutineManager(bot.ExchangeManager, bot.OrderManager, bot.currencyPairSyncer, &bot.Config.Currency, bot.Settings.Verbose) + if err != nil { + gctlog.Errorf(gctlog.Global, "Unable to initialise websocket routine manager. Err: %s", err) + } else { + err = bot.websocketRoutineManager.Start() + if err != nil { + gctlog.Errorf(gctlog.Global, "failed to start websocket routine manager. Err: %s", err) + } + } } if bot.Settings.EnableGCTScriptManager { - if err := bot.GctScriptManager.Start(&bot.ServicesWG); err != nil { - gctlog.Errorf(gctlog.Global, "GCTScript manager unable to start: %v", err) + bot.gctScriptManager, err = gctscript.NewManager(&bot.Config.GCTScript) + if err != nil { + gctlog.Errorf(gctlog.Global, "failed to create script manager. Err: %s", err) + } + if err := bot.gctScriptManager.Start(&bot.ServicesWG); err != nil { + gctlog.Errorf(gctlog.Global, "GCTScript manager unable to start: %s", err) } } @@ -511,46 +613,64 @@ func (bot *Engine) Stop() { gctlog.Debugln(gctlog.Global, "Engine shutting down..") - if len(portfolio.Portfolio.Addresses) != 0 { - bot.Config.Portfolio = portfolio.Portfolio + if len(bot.portfolioManager.GetAddresses()) != 0 { + bot.Config.Portfolio = *bot.portfolioManager.GetPortfolio() } - if bot.GctScriptManager.Started() { - if err := bot.GctScriptManager.Stop(); err != nil { + if bot.gctScriptManager.IsRunning() { + if err := bot.gctScriptManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "GCTScript manager unable to stop. Error: %v", err) } } - if bot.OrderManager.Started() { + if bot.OrderManager.IsRunning() { if err := bot.OrderManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "Order manager unable to stop. Error: %v", err) } } - if bot.NTPManager.Started() { - if err := bot.NTPManager.Stop(); err != nil { + if bot.eventManager.IsRunning() { + if err := bot.eventManager.Stop(); err != nil { + gctlog.Errorf(gctlog.Global, "event manager unable to stop. Error: %v", err) + } + } + + if bot.ntpManager.IsRunning() { + if err := bot.ntpManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "NTP manager unable to stop. Error: %v", err) } } - if bot.CommsManager.Started() { - if err := bot.CommsManager.Stop(); err != nil { + if bot.CommunicationsManager.IsRunning() { + if err := bot.CommunicationsManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "Communication manager unable to stop. Error: %v", err) } } - if bot.PortfolioManager.Started() { - if err := bot.PortfolioManager.Stop(); err != nil { + if bot.portfolioManager.IsRunning() { + if err := bot.portfolioManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "Fund manager unable to stop. Error: %v", err) } } - if bot.ConnectionManager.Started() { - if err := bot.ConnectionManager.Stop(); err != nil { + if bot.connectionManager.IsRunning() { + if err := bot.connectionManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "Connection manager unable to stop. Error: %v", err) } } - if bot.DatabaseManager.Started() { + 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.DatabaseManager.IsRunning() { if err := bot.DatabaseManager.Stop(); err != nil { gctlog.Errorf(gctlog.Global, "Database manager unable to stop. Error: %v", err) } @@ -561,6 +681,11 @@ func (bot *Engine) Stop() { gctlog.Errorf(gctlog.DispatchMgr, "Dispatch system unable to stop. Error: %v", err) } } + if bot.websocketRoutineManager.IsRunning() { + if err := bot.websocketRoutineManager.Stop(); err != nil { + gctlog.Errorf(gctlog.Global, "websocket routine manager unable to stop. Error: %v", err) + } + } if bot.Settings.EnableCoinmarketcapAnalysis || bot.Settings.EnableCurrencyConverter || @@ -589,3 +714,233 @@ func (bot *Engine) Stop() { log.Printf("Failed to close logger. Error: %v\n", err) } } + +// GetExchangeByName returns an exchange given an exchange name +func (bot *Engine) GetExchangeByName(exchName string) exchange.IBotExchange { + return bot.ExchangeManager.GetExchangeByName(exchName) +} + +// UnloadExchange unloads an exchange by name +func (bot *Engine) UnloadExchange(exchName string) error { + exchCfg, err := bot.Config.GetExchangeConfig(exchName) + if err != nil { + return err + } + + err = bot.ExchangeManager.RemoveExchange(exchName) + if err != nil { + return err + } + + exchCfg.Enabled = false + return nil +} + +// GetExchanges retrieves the loaded exchanges +func (bot *Engine) GetExchanges() []exchange.IBotExchange { + return bot.ExchangeManager.GetExchanges() +} + +// LoadExchange loads an exchange by name +func (bot *Engine) LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { + exch, err := bot.ExchangeManager.NewExchangeByName(name) + if err != nil { + return err + } + if exch.GetBase() == nil { + return ErrExchangeFailedToLoad + } + + var localWG sync.WaitGroup + localWG.Add(1) + go func() { + exch.SetDefaults() + localWG.Done() + }() + exchCfg, err := bot.Config.GetExchangeConfig(name) + if err != nil { + return err + } + + if bot.Settings.EnableAllPairs && + exchCfg.CurrencyPairs != nil { + assets := exchCfg.CurrencyPairs.GetAssetTypes() + for x := range assets { + var pairs currency.Pairs + pairs, err = exchCfg.CurrencyPairs.GetPairs(assets[x], false) + if err != nil { + return err + } + exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true) + } + } + + if bot.Settings.EnableExchangeVerbose { + exchCfg.Verbose = true + } + if exchCfg.Features != nil { + if bot.Settings.EnableExchangeWebsocketSupport && + exchCfg.Features.Supports.Websocket { + exchCfg.Features.Enabled.Websocket = true + } + if bot.Settings.EnableExchangeAutoPairUpdates && + exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates { + exchCfg.Features.Enabled.AutoPairUpdates = true + } + if bot.Settings.DisableExchangeAutoPairUpdates { + if exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates { + exchCfg.Features.Enabled.AutoPairUpdates = false + } + } + } + if bot.Settings.HTTPUserAgent != "" { + exchCfg.HTTPUserAgent = bot.Settings.HTTPUserAgent + } + if bot.Settings.HTTPProxy != "" { + exchCfg.ProxyAddress = bot.Settings.HTTPProxy + } + if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout { + exchCfg.HTTPTimeout = bot.Settings.HTTPTimeout + } + if bot.Settings.EnableExchangeHTTPDebugging { + exchCfg.HTTPDebugging = bot.Settings.EnableExchangeHTTPDebugging + } + + localWG.Wait() + if !bot.Settings.EnableExchangeHTTPRateLimiter { + gctlog.Warnf(gctlog.ExchangeSys, + "Loaded exchange %s rate limiting has been turned off.\n", + exch.GetName(), + ) + err = exch.DisableRateLimiter() + if err != nil { + gctlog.Errorf(gctlog.ExchangeSys, + "Loaded exchange %s rate limiting cannot be turned off: %s.\n", + exch.GetName(), + err, + ) + } + } + + exchCfg.Enabled = true + err = exch.Setup(exchCfg) + if err != nil { + exchCfg.Enabled = false + return err + } + + bot.ExchangeManager.Add(exch) + base := exch.GetBase() + if base.API.AuthenticatedSupport || + base.API.AuthenticatedWebsocketSupport { + assetTypes := base.GetAssetTypes() + var useAsset asset.Item + for a := range assetTypes { + err = base.CurrencyPairs.IsAssetEnabled(assetTypes[a]) + if err != nil { + continue + } + useAsset = assetTypes[a] + break + } + err = exch.ValidateCredentials(useAsset) + if err != nil { + gctlog.Warnf(gctlog.ExchangeSys, + "%s: Cannot validate credentials, authenticated support has been disabled, Error: %s\n", + base.Name, + err) + base.API.AuthenticatedSupport = false + base.API.AuthenticatedWebsocketSupport = false + exchCfg.API.AuthenticatedSupport = false + exchCfg.API.AuthenticatedWebsocketSupport = false + } + } + + if useWG { + exch.Start(wg) + } else { + tempWG := sync.WaitGroup{} + exch.Start(&tempWG) + tempWG.Wait() + } + + return nil +} + +func (bot *Engine) dryRunParamInteraction(param string) { + if !bot.Settings.CheckParamInteraction { + return + } + + if !bot.Settings.EnableDryRun { + gctlog.Warnf(gctlog.Global, + "Command line argument '-%s' induces dry run mode."+ + " Set -dryrun=false if you wish to override this.", + param) + bot.Settings.EnableDryRun = true + } +} + +// SetupExchanges sets up the exchanges used by the Bot +func (bot *Engine) SetupExchanges() error { + var wg sync.WaitGroup + configs := bot.Config.GetAllExchangeConfigs() + if bot.Settings.EnableAllPairs { + bot.dryRunParamInteraction("enableallpairs") + } + if bot.Settings.EnableAllExchanges { + bot.dryRunParamInteraction("enableallexchanges") + } + if bot.Settings.EnableExchangeVerbose { + bot.dryRunParamInteraction("exchangeverbose") + } + if bot.Settings.EnableExchangeWebsocketSupport { + bot.dryRunParamInteraction("exchangewebsocketsupport") + } + if bot.Settings.EnableExchangeAutoPairUpdates { + bot.dryRunParamInteraction("exchangeautopairupdates") + } + if bot.Settings.DisableExchangeAutoPairUpdates { + bot.dryRunParamInteraction("exchangedisableautopairupdates") + } + if bot.Settings.HTTPUserAgent != "" { + bot.dryRunParamInteraction("httpuseragent") + } + if bot.Settings.HTTPProxy != "" { + bot.dryRunParamInteraction("httpproxy") + } + if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout { + bot.dryRunParamInteraction("httptimeout") + } + if bot.Settings.EnableExchangeHTTPDebugging { + bot.dryRunParamInteraction("exchangehttpdebugging") + } + + for x := range configs { + if !configs[x].Enabled && !bot.Settings.EnableAllExchanges { + gctlog.Debugf(gctlog.ExchangeSys, "%s: Exchange support: Disabled\n", configs[x].Name) + continue + } + wg.Add(1) + cfg := configs[x] + go func(currCfg config.ExchangeConfig) { + defer wg.Done() + err := bot.LoadExchange(currCfg.Name, true, &wg) + if err != nil { + gctlog.Errorf(gctlog.ExchangeSys, "LoadExchange %s failed: %s\n", currCfg.Name, err) + return + } + gctlog.Debugf(gctlog.ExchangeSys, + "%s: Exchange support: Enabled (Authenticated API support: %s - Verbose mode: %s).\n", + currCfg.Name, + common.IsEnabled(currCfg.API.AuthenticatedSupport), + common.IsEnabled(currCfg.Verbose), + ) + }(cfg) + } + wg.Wait() + if len(bot.ExchangeManager.GetExchanges()) == 0 { + return ErrNoExchangesLoaded + } + return nil +} diff --git a/engine/engine_test.go b/engine/engine_test.go index bdb68778..f71d62ca 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1,6 +1,8 @@ package engine import ( + "errors" + "io/ioutil" "os" "testing" @@ -81,7 +83,7 @@ func TestStartStopDoesNotCausePanic(t *testing.T) { if err != nil { t.Error(err) } - + botOne.Settings.EnableGRPCProxy = false if err = botOne.Start(); err != nil { t.Error(err) } @@ -89,23 +91,51 @@ func TestStartStopDoesNotCausePanic(t *testing.T) { botOne.Stop() } +var enableExperimentalTest = false + func TestStartStopTwoDoesNotCausePanic(t *testing.T) { - t.Skip("Closing global currency.storage from two bots causes panic") t.Parallel() + if !enableExperimentalTest { + t.Skip("test is functional, however does not need to be included in go test runs") + } + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err) + } + tempDir2, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err) + } + defer func() { + err = os.RemoveAll(tempDir) + if err != nil { + t.Error(err) + } + err = os.RemoveAll(tempDir2) + if err != nil { + t.Error(err) + } + }() botOne, err := NewFromSettings(&Settings{ ConfigFile: config.TestFile, EnableDryRun: true, + DataDir: tempDir, }, nil) if err != nil { t.Error(err) } + botOne.Settings.EnableGRPCProxy = false + botTwo, err := NewFromSettings(&Settings{ ConfigFile: config.TestFile, EnableDryRun: true, + DataDir: tempDir2, }, nil) if err != nil { t.Error(err) } + botTwo.Settings.EnableGRPCProxy = false + if err = botOne.Start(); err != nil { t.Error(err) } @@ -116,3 +146,113 @@ func TestStartStopTwoDoesNotCausePanic(t *testing.T) { botOne.Stop() botTwo.Stop() } + +func TestCheckExchangeExists(t *testing.T) { + e := CreateTestBot(t) + + if e.GetExchangeByName(testExchange) == nil { + t.Errorf("TestGetExchangeExists: Unable to find exchange") + } + + if e.GetExchangeByName("Asdsad") != nil { + t.Errorf("TestGetExchangeExists: Non-existent exchange found") + } +} + +func TestGetExchangeByName(t *testing.T) { + e := CreateTestBot(t) + + exch := e.GetExchangeByName(testExchange) + if exch == nil { + t.Errorf("TestGetExchangeByName: Failed to get exchange") + } + + if !exch.IsEnabled() { + t.Errorf("TestGetExchangeByName: Unexpected result") + } + + exch.SetEnabled(false) + bfx := e.GetExchangeByName(testExchange) + if bfx.IsEnabled() { + t.Errorf("TestGetExchangeByName: Unexpected result") + } + + if exch.GetName() != testExchange { + t.Errorf("TestGetExchangeByName: Unexpected result") + } + + exch = e.GetExchangeByName("Asdasd") + if exch != nil { + t.Errorf("TestGetExchangeByName: Non-existent exchange found") + } +} + +func TestUnloadExchange(t *testing.T) { + e := CreateTestBot(t) + + err := e.UnloadExchange("asdf") + if !errors.Is(err, config.ErrExchangeNotFound) { + t.Errorf("error '%v', expected '%v'", err, config.ErrExchangeNotFound) + } + + err = e.UnloadExchange(testExchange) + if err != nil { + t.Errorf("TestUnloadExchange: Failed to get exchange. %s", + err) + } + + err = e.UnloadExchange(testExchange) + if !errors.Is(err, ErrNoExchangesLoaded) { + t.Errorf("error '%v', expected '%v'", err, ErrNoExchangesLoaded) + } +} + +func TestDryRunParamInteraction(t *testing.T) { + bot := CreateTestBot(t) + + // Simulate overiding default settings and ensure that enabling exchange + // verbose mode will be set on Bitfinex + var err error + if err = bot.UnloadExchange(testExchange); err != nil { + t.Error(err) + } + + bot.Settings.CheckParamInteraction = false + bot.Settings.EnableExchangeVerbose = false + if err = bot.LoadExchange(testExchange, false, nil); err != nil { + t.Error(err) + } + + exchCfg, err := bot.Config.GetExchangeConfig(testExchange) + if err != nil { + t.Error(err) + } + + if exchCfg.Verbose { + t.Error("verbose should have been disabled") + } + + if err = bot.UnloadExchange(testExchange); err != nil { + t.Error(err) + } + + // Now set dryrun mode to true, + // enable exchange verbose mode and verify that verbose mode + // will be set on Bitfinex + bot.Settings.EnableDryRun = true + bot.Settings.CheckParamInteraction = true + bot.Settings.EnableExchangeVerbose = true + if err = bot.LoadExchange(testExchange, false, nil); err != nil { + t.Error(err) + } + + exchCfg, err = bot.Config.GetExchangeConfig(testExchange) + if err != nil { + t.Error(err) + } + + if !bot.Settings.EnableDryRun || + !exchCfg.Verbose { + t.Error("dryrun should be true and verbose should be true") + } +} diff --git a/engine/engine_types.go b/engine/engine_types.go index e9b258ff..16a1c001 100644 --- a/engine/engine_types.go +++ b/engine/engine_types.go @@ -96,6 +96,8 @@ const ( MsgStatusSuccess string = "success" // MsgStatusError message to display when failure occurs MsgStatusError string = "error" + grpcName string = "grpc" + grpcProxyName string = "grpc_proxy" ) // newConfigMutex only locks and unlocks on engine creation functions diff --git a/engine/event_manager.go b/engine/event_manager.go new file mode 100644 index 00000000..a328aafc --- /dev/null +++ b/engine/event_manager.go @@ -0,0 +1,334 @@ +package engine + +import ( + "errors" + "fmt" + "strings" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/currency" + "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" +) + +// setupEventManager loads and validates the communications manager config +func setupEventManager(comManager iCommsManager, exchangeManager iExchangeManager, sleepDelay time.Duration, verbose bool) (*eventManager, error) { + if comManager == nil { + return nil, errNilComManager + } + if exchangeManager == nil { + return nil, errNilExchangeManager + } + if sleepDelay <= 0 { + sleepDelay = EventSleepDelay + } + return &eventManager{ + comms: comManager, + exchangeManager: exchangeManager, + verbose: verbose, + sleepDelay: sleepDelay, + shutdown: make(chan struct{}), + }, nil +} + +// Start runs the subsystem +func (m *eventManager) Start() error { + if m == nil { + return fmt.Errorf("event manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("event manager %w", ErrSubSystemAlreadyStarted) + } + log.Debugf(log.EventMgr, "Event Manager started. SleepDelay: %v\n", EventSleepDelay.String()) + m.shutdown = make(chan struct{}) + go m.run() + return nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *eventManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Stop attempts to shutdown the subsystem +func (m *eventManager) Stop() error { + if m == nil { + return fmt.Errorf("event manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 1, 0) { + return fmt.Errorf("event manager %w", ErrSubSystemNotStarted) + } + close(m.shutdown) + return nil +} + +func (m *eventManager) run() { + t := time.NewTicker(m.sleepDelay) + select { + case <-m.shutdown: + return + case <-t.C: + total, executed := m.getEventCounter() + if total > 0 && executed != total { + m.m.Lock() + for i := range m.events { + m.executeEvent(i) + } + m.m.Unlock() + } + } +} + +func (m *eventManager) executeEvent(i int) { + if !m.events[i].Executed { + if m.verbose { + log.Debugf(log.EventMgr, "Events: Processing event %s.\n", m.events[i].String()) + } + err := m.checkEventCondition(&m.events[i]) + if err != nil { + msg := fmt.Sprintf( + "Events: ID: %d triggered on %s successfully [%v]\n", m.events[i].ID, + m.events[i].Exchange, m.events[i].String(), + ) + log.Infoln(log.EventMgr, msg) + m.comms.PushEvent(base.Event{Type: "event", Message: msg}) + m.events[i].Executed = true + } else if m.verbose { + log.Debugf(log.EventMgr, "%v", err) + } + } +} + +// Add adds an event to the Events chain and returns an index/eventID +// and an error +func (m *eventManager) Add(exchange, item string, condition EventConditionParams, p currency.Pair, a asset.Item, action string) (int64, error) { + if m == nil { + return 0, fmt.Errorf("event manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return 0, fmt.Errorf("event manager %w", ErrSubSystemNotStarted) + } + err := m.isValidEvent(exchange, item, condition, action) + if err != nil { + return 0, err + } + evt := Event{ + Exchange: exchange, + Item: item, + Condition: condition, + Pair: p, + Asset: a, + Action: action, + Executed: false, + } + m.m.Lock() + if len(m.events) > 0 { + evt.ID = int64(len(m.events) + 1) + } + m.events = append(m.events, evt) + m.m.Unlock() + + return evt.ID, nil +} + +// Remove deletes an event by its ID +func (m *eventManager) Remove(eventID int64) bool { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return false + } + m.m.Lock() + defer m.m.Unlock() + for i := range m.events { + if m.events[i].ID == eventID { + m.events = append(m.events[:i], m.events[i+1:]...) + return true + } + } + return false +} + +// getEventCounter displays the amount of total events on the chain and the +// events that have been executed. +func (m *eventManager) getEventCounter() (total, executed int) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return 0, 0 + } + m.m.Lock() + defer m.m.Unlock() + total = len(m.events) + for i := range m.events { + if m.events[i].Executed { + executed++ + } + } + return total, executed +} + +// checkEventCondition will check the event structure to see if there is a condition +// met +func (m *eventManager) checkEventCondition(e *Event) error { + if m == nil { + return fmt.Errorf("event manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("event manager %w", ErrSubSystemNotStarted) + } + if e == nil { + return errNilEvent + } + if e.Item == ItemPrice { + return e.processTicker() + } + return e.processOrderbook() +} + +// isValidEvent checks the actions to be taken and returns an error if incorrect +func (m *eventManager) isValidEvent(exchange, item string, condition EventConditionParams, action string) error { + exchange = strings.ToUpper(exchange) + item = strings.ToUpper(item) + action = strings.ToUpper(action) + + if !m.isValidExchange(exchange) { + return errExchangeDisabled + } + + if !isValidItem(item) { + return errInvalidItem + } + + if !isValidCondition(condition.Condition) { + return errInvalidCondition + } + + if item == ItemPrice { + if condition.Price <= 0 { + return errInvalidCondition + } + } + + if item == ItemOrderbook { + if condition.OrderbookAmount <= 0 { + return errInvalidCondition + } + } + + if strings.Contains(action, ",") { + a := strings.Split(action, ",") + + if a[0] != ActionSMSNotify { + return errInvalidAction + } + } else if action != ActionConsolePrint && action != ActionTest { + return errInvalidAction + } + + return nil +} + +// isValidExchange validates the exchange +func (m *eventManager) isValidExchange(exchangeName string) bool { + return m.exchangeManager.GetExchangeByName(exchangeName) != nil +} + +// isValidCondition validates passed in condition +func isValidCondition(condition string) bool { + switch condition { + case ConditionGreaterThan, ConditionGreaterThanOrEqual, ConditionLessThan, ConditionLessThanOrEqual, ConditionIsEqual: + return true + } + return false +} + +// isValidItem validates passed in Item +func isValidItem(item string) bool { + item = strings.ToUpper(item) + switch item { + case ItemPrice, ItemOrderbook: + return true + } + return false +} + +// String turns the structure event into a string +func (e *Event) String() string { + return fmt.Sprintf( + "If the %s [%s] %s on %s meets the following %v then %s.", e.Pair.String(), + strings.ToUpper(e.Asset.String()), e.Item, e.Exchange, e.Condition, e.Action, + ) +} + +func (e *Event) processTicker() error { + t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset) + if err != nil { + return fmt.Errorf("failed to get ticker. Err: %w", err) + } + + if t.Last == 0 { + return errTickerLastPriceZero + } + return e.shouldProcessEvent(t.Last, e.Condition.Price) +} + +func (e *Event) shouldProcessEvent(actual, threshold float64) error { + switch e.Condition.Condition { + case ConditionGreaterThan: + if actual > threshold { + return nil + } + case ConditionGreaterThanOrEqual: + if actual >= threshold { + return nil + } + case ConditionLessThan: + if actual < threshold { + return nil + } + case ConditionLessThanOrEqual: + if actual <= threshold { + return nil + } + case ConditionIsEqual: + if actual == threshold { + return nil + } + } + return errors.New("does not meet conditions") +} + +func (e *Event) processOrderbook() error { + ob, err := orderbook.Get(e.Exchange, e.Pair, e.Asset) + if err != nil { + return fmt.Errorf("events: Failed to get orderbook. Err: %w", err) + } + if !e.Condition.CheckBids && !e.Condition.CheckAsks { + return nil + } + + if e.Condition.CheckBids { + for x := range ob.Bids { + subtotal := ob.Bids[x].Amount * ob.Bids[x].Price + err = e.shouldProcessEvent(subtotal, e.Condition.OrderbookAmount) + if err == nil { + log.Debugf(log.EventMgr, "Events: Bid Amount: %f Price: %v Subtotal: %v\n", ob.Bids[x].Amount, ob.Bids[x].Price, subtotal) + } + } + } + + if e.Condition.CheckAsks { + for x := range ob.Asks { + subtotal := ob.Asks[x].Amount * ob.Asks[x].Price + err = e.shouldProcessEvent(subtotal, e.Condition.OrderbookAmount) + if err == nil { + log.Debugf(log.EventMgr, "Events: Ask Amount: %f Price: %v Subtotal: %v\n", ob.Asks[x].Amount, ob.Asks[x].Price, subtotal) + } + } + } + return err +} diff --git a/engine/event_manager.md b/engine/event_manager.md new file mode 100644 index 00000000..b88a22a1 --- /dev/null +++ b/engine/event_manager.md @@ -0,0 +1,52 @@ +# GoCryptoTrader package Event_manager + + + + +[![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/event_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This event_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Event_manager ++ The event manager subsystem is used to push events to communication systems such as Slack ++ The only configurable aspects of the event manager are the delays between receiving an event and pushing it and enabling verbose: + +### connectionMonitor + +| Config | Description | Example | +| ------ | ----------- | ------- | +| eventmanagerdelay | Sets the event managers sleep delay between event checking by a Golang `time.Duration` | `0` | +| verbose | Outputs debug messaging allowing for greater transparency for what the event manager is doing | `false` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/event_manager_test.go b/engine/event_manager_test.go new file mode 100644 index 00000000..011297f2 --- /dev/null +++ b/engine/event_manager_test.go @@ -0,0 +1,321 @@ +package engine + +import ( + "errors" + "strings" + "sync/atomic" + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +func TestSetupEventManager(t *testing.T) { + t.Parallel() + _, err := setupEventManager(nil, nil, 0, false) + if !errors.Is(err, errNilComManager) { + t.Errorf("error '%v', expected '%v'", err, errNilComManager) + } + + _, err = setupEventManager(&CommunicationManager{}, nil, 0, false) + if !errors.Is(err, errNilExchangeManager) { + t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) + } + + m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false) + if !errors.Is(err, nil) { + t.Fatalf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Fatal("expected manager") + } + if m.sleepDelay == 0 { + t.Error("expected default set") + } +} + +func TestEventManagerStart(t *testing.T) { + m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } + + m = nil + err = m.Start() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestEventManagerIsRunning(t *testing.T) { + t.Parallel() + m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if !m.IsRunning() { + t.Error("expected true") + } + atomic.StoreInt32(&m.started, 0) + if m.IsRunning() { + t.Error("expected false") + } + m = nil + if m.IsRunning() { + t.Error("expected false") + } +} + +func TestEventManagerStop(t *testing.T) { + t.Parallel() + m, err := setupEventManager(&CommunicationManager{}, &ExchangeManager{}, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + m = nil + err = m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestEventManagerAdd(t *testing.T) { + t.Parallel() + em := SetupExchangeManager() + m, err := setupEventManager(&CommunicationManager{}, em, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + _, err = m.Add("", "", EventConditionParams{}, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "") + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + _, err = m.Add("", "", EventConditionParams{}, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "") + if !errors.Is(err, errExchangeDisabled) { + t.Errorf("error '%v', expected '%v'", err, errExchangeDisabled) + } + exch, err := em.NewExchangeByName(testExchange) + if err != nil { + t.Error(err) + } + exch.SetDefaults() + em.Add(exch) + _, err = m.Add(testExchange, "", EventConditionParams{}, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "") + if !errors.Is(err, errInvalidItem) { + t.Errorf("error '%v', expected '%v'", err, errInvalidItem) + } + + cond := EventConditionParams{ + Condition: ConditionGreaterThan, + Price: 1337, + OrderbookAmount: 1337, + } + _, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, "") + if !errors.Is(err, errInvalidAction) { + t.Errorf("error '%v', expected '%v'", err, errInvalidAction) + } + + _, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, ActionTest) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + action := ActionSMSNotify + "," + ActionTest + _, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, action) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestEventManagerRemove(t *testing.T) { + t.Parallel() + em := SetupExchangeManager() + m, err := setupEventManager(&CommunicationManager{}, em, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.Remove(0) { + t.Error("expected false") + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.Remove(0) { + t.Error("expected false") + } + action := ActionSMSNotify + "," + ActionTest + cond := EventConditionParams{ + Condition: ConditionGreaterThan, + Price: 1337, + OrderbookAmount: 1337, + } + exch, err := em.NewExchangeByName(testExchange) + if err != nil { + t.Error(err) + } + exch.SetDefaults() + em.Add(exch) + id, err := m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, action) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + if !m.Remove(id) { + t.Error("expected true") + } +} + +func TestGetEventCounter(t *testing.T) { + t.Parallel() + em := SetupExchangeManager() + m, err := setupEventManager(&CommunicationManager{}, em, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + total, executed := m.getEventCounter() + if total != 0 && executed != 0 { + t.Error("expected 0") + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + total, executed = m.getEventCounter() + if total != 0 && executed != 0 { + t.Error("expected 0") + } + action := ActionSMSNotify + "," + ActionTest + cond := EventConditionParams{ + Condition: ConditionGreaterThan, + Price: 1337, + OrderbookAmount: 1337, + } + exch, err := em.NewExchangeByName(testExchange) + if err != nil { + t.Error(err) + } + exch.SetDefaults() + em.Add(exch) + _, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USDC), asset.Spot, action) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + total, _ = m.getEventCounter() + if total == 0 { + t.Error("expected 1") + } +} + +func TestCheckEventCondition(t *testing.T) { + em := SetupExchangeManager() + m, err := setupEventManager(&CommunicationManager{}, em, 0, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Lock() + err = m.checkEventCondition(nil) + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + m.m.Unlock() + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Lock() + err = m.checkEventCondition(nil) + if !errors.Is(err, errNilEvent) { + t.Errorf("error '%v', expected '%v'", err, errNilEvent) + } + m.m.Unlock() + + action := ActionSMSNotify + "," + ActionTest + cond := EventConditionParams{ + Condition: ConditionGreaterThan, + Price: 1337, + OrderbookAmount: 1337, + } + exch, err := em.NewExchangeByName(testExchange) + if err != nil { + t.Error(err) + } + exch.SetDefaults() + em.Add(exch) + _, err = m.Add(testExchange, ItemPrice, cond, currency.NewPair(currency.BTC, currency.USD), asset.Spot, action) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Lock() + err = m.checkEventCondition(&m.events[0]) + if err != nil && !strings.Contains(err.Error(), "no tickers for") { + t.Error(err) + } else if err == nil { + t.Error("expected error") + } + m.m.Unlock() + _, err = exch.FetchTicker(currency.NewPair(currency.BTC, currency.USD), asset.Spot) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Lock() + err = m.checkEventCondition(&m.events[0]) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Unlock() + + m.events[0].Item = ItemOrderbook + m.events[0].Executed = false + m.events[0].Condition.CheckAsks = true + m.events[0].Condition.CheckBids = true + m.m.Lock() + err = m.checkEventCondition(&m.events[0]) + if err != nil && !strings.Contains(err.Error(), "cannot find orderbook") { + t.Error(err) + } else if err == nil { + t.Error("expected error") + } + m.m.Unlock() + + _, err = exch.FetchOrderbook(currency.NewPair(currency.BTC, currency.USD), asset.Spot) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Lock() + err = m.checkEventCondition(&m.events[0]) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + m.m.Unlock() +} diff --git a/engine/event_manager_types.go b/engine/event_manager_types.go new file mode 100644 index 00000000..39bb00ce --- /dev/null +++ b/engine/event_manager_types.go @@ -0,0 +1,74 @@ +package engine + +import ( + "errors" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// Event const vars +const ( + ItemPrice = "PRICE" + ItemOrderbook = "ORDERBOOK" + + ConditionGreaterThan = ">" + ConditionGreaterThanOrEqual = ">=" + ConditionLessThan = "<" + ConditionLessThanOrEqual = "<=" + ConditionIsEqual = "==" + + ActionSMSNotify = "SMS" + ActionConsolePrint = "CONSOLE_PRINT" + ActionTest = "ACTION_TEST" + + defaultSleepDelay = time.Millisecond * 500 +) + +// vars related to events package +var ( + EventSleepDelay = defaultSleepDelay + errInvalidItem = errors.New("invalid item") + errInvalidCondition = errors.New("invalid conditional option") + errInvalidAction = errors.New("invalid action") + errExchangeDisabled = errors.New("desired exchange is disabled") + errNilEvent = errors.New("nil event received") + errNilComManager = errors.New("nil communications manager received") + errTickerLastPriceZero = errors.New("ticker last price is 0") +) + +// EventConditionParams holds the event condition variables +type EventConditionParams struct { + Condition string + Price float64 + + CheckBids bool + CheckAsks bool + OrderbookAmount float64 +} + +// Event struct holds the event variables +type Event struct { + ID int64 + Exchange string + Item string + Condition EventConditionParams + Pair currency.Pair + Asset asset.Item + Action string + Executed bool +} + +// eventManager holds communication manager data +type eventManager struct { + started int32 + comms iCommsManager + events []Event + verbose bool + sleepDelay time.Duration + exchangeManager iExchangeManager + shutdown chan struct{} + m sync.Mutex +} diff --git a/engine/events.go b/engine/events.go deleted file mode 100644 index 92bab065..00000000 --- a/engine/events.go +++ /dev/null @@ -1,347 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "strings" - "time" - - "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - "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" -) - -// TO-DO MAKE THIS A SERVICE SUBSYSTEM - -// Event const vars -const ( - ItemPrice = "PRICE" - ItemOrderbook = "ORDERBOOK" - - ConditionGreaterThan = ">" - ConditionGreaterThanOrEqual = ">=" - ConditionLessThan = "<" - ConditionLessThanOrEqual = "<=" - ConditionIsEqual = "==" - - ActionSMSNotify = "SMS" - ActionConsolePrint = "CONSOLE_PRINT" - ActionTest = "ACTION_TEST" - - defaultSleepDelay = time.Millisecond * 500 -) - -// vars related to events package -var ( - errInvalidItem = errors.New("invalid item") - errInvalidCondition = errors.New("invalid conditional option") - errInvalidAction = errors.New("invalid action") - errExchangeDisabled = errors.New("desired exchange is disabled") - EventSleepDelay = defaultSleepDelay -) - -// EventConditionParams holds the event condition variables -type EventConditionParams struct { - Condition string - Price float64 - - CheckBids bool - CheckBidsAndAsks bool - OrderbookAmount float64 -} - -// Event struct holds the event variables -type Event struct { - ID int64 - Exchange string - Item string - Condition EventConditionParams - Pair currency.Pair - Asset asset.Item - Action string - Executed bool -} - -// Events variable is a pointer array to the event structures that will be -// appended -var Events []*Event - -// Add adds an event to the Events chain and returns an index/eventID -// and an error -func Add(exchange, item string, condition EventConditionParams, p currency.Pair, a asset.Item, action string) (int64, error) { - err := IsValidEvent(exchange, item, condition, action) - if err != nil { - return 0, err - } - - evt := &Event{} - - if len(Events) == 0 { - evt.ID = 0 - } else { - evt.ID = int64(len(Events) + 1) - } - - evt.Exchange = exchange - evt.Item = item - evt.Condition = condition - evt.Pair = p - evt.Asset = a - evt.Action = action - evt.Executed = false - Events = append(Events, evt) - return evt.ID, nil -} - -// Remove deletes and event by its ID -func Remove(eventID int64) bool { - for i := range Events { - if Events[i].ID == eventID { - Events = append(Events[:i], Events[i+1:]...) - return true - } - } - return false -} - -// GetEventCounter displays the emount of total events on the chain and the -// events that have been executed. -func GetEventCounter() (total, executed int) { - total = len(Events) - for i := range Events { - if Events[i].Executed { - executed++ - } - } - return total, executed -} - -// ExecuteAction will execute the action pending on the chain -func (e *Event) ExecuteAction() bool { - if strings.Contains(e.Action, ",") { - action := strings.Split(e.Action, ",") - if action[0] == ActionSMSNotify { - if action[1] == "ALL" { - Bot.CommsManager.PushEvent(base.Event{ - Type: "event", - Message: "Event triggered: " + e.String(), - }) - } - } - } else { - log.Debugf(log.EventMgr, "Event triggered: %s\n", e.String()) - } - return true -} - -// String turns the structure event into a string -func (e *Event) String() string { - return fmt.Sprintf( - "If the %s [%s] %s on %s meets the following %v then %s.", e.Pair.String(), - strings.ToUpper(e.Asset.String()), e.Item, e.Exchange, e.Condition, e.Action, - ) -} - -func (e *Event) processTicker(verbose bool) bool { - t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset) - if err != nil { - if verbose { - log.Debugf(log.EventMgr, "Events: failed to get ticker. Err: %s\n", err) - } - return false - } - - if t.Last == 0 { - if verbose { - log.Debugln(log.EventMgr, "Events: ticker last price is 0") - } - return false - } - return e.processCondition(t.Last, e.Condition.Price) -} - -func (e *Event) processCondition(actual, threshold float64) bool { - switch e.Condition.Condition { - case ConditionGreaterThan: - if actual > threshold { - return e.ExecuteAction() - } - case ConditionGreaterThanOrEqual: - if actual >= threshold { - return e.ExecuteAction() - } - case ConditionLessThan: - if actual < threshold { - return e.ExecuteAction() - } - case ConditionLessThanOrEqual: - if actual <= threshold { - return e.ExecuteAction() - } - case ConditionIsEqual: - if actual == threshold { - return e.ExecuteAction() - } - } - return false -} - -func (e *Event) processOrderbook(verbose bool) bool { - ob, err := orderbook.Get(e.Exchange, e.Pair, e.Asset) - if err != nil { - if verbose { - log.Debugf(log.EventMgr, "Events: Failed to get orderbook. Err: %s\n", err) - } - return false - } - - success := false - if e.Condition.CheckBids || e.Condition.CheckBidsAndAsks { - for x := range ob.Bids { - subtotal := ob.Bids[x].Amount * ob.Bids[x].Price - result := e.processCondition(subtotal, e.Condition.OrderbookAmount) - if result { - success = true - log.Debugf(log.EventMgr, "Events: Bid Amount: %f Price: %v Subtotal: %v\n", ob.Bids[x].Amount, ob.Bids[x].Price, subtotal) - } - } - } - - if !e.Condition.CheckBids || e.Condition.CheckBidsAndAsks { - for x := range ob.Asks { - subtotal := ob.Asks[x].Amount * ob.Asks[x].Price - result := e.processCondition(subtotal, e.Condition.OrderbookAmount) - if result { - success = true - log.Debugf(log.EventMgr, "Events: Ask Amount: %f Price: %v Subtotal: %v\n", ob.Asks[x].Amount, ob.Asks[x].Price, subtotal) - } - } - } - return success -} - -// CheckEventCondition will check the event structure to see if there is a condition -// met -func (e *Event) CheckEventCondition(verbose bool) bool { - if e.Item == ItemPrice { - return e.processTicker(verbose) - } - return e.processOrderbook(verbose) -} - -// IsValidEvent checks the actions to be taken and returns an error if incorrect -func IsValidEvent(exchange, item string, condition EventConditionParams, action string) error { - exchange = strings.ToUpper(exchange) - item = strings.ToUpper(item) - action = strings.ToUpper(action) - - if !IsValidExchange(exchange) { - return errExchangeDisabled - } - - if !IsValidItem(item) { - return errInvalidItem - } - - if !IsValidCondition(condition.Condition) { - return errInvalidCondition - } - - if item == ItemPrice { - if condition.Price <= 0 { - return errInvalidCondition - } - } - - if item == ItemOrderbook { - if condition.OrderbookAmount <= 0 { - return errInvalidCondition - } - } - - if strings.Contains(action, ",") { - a := strings.Split(action, ",") - - if a[0] != ActionSMSNotify { - return errInvalidAction - } - } else if action != ActionConsolePrint && action != ActionTest { - return errInvalidAction - } - - return nil -} - -// EventManger is the overarching routine that will iterate through the Events -// chain -func EventManger(verbose bool, comManager *commsManager) { - log.Debugf(log.EventMgr, "EventManager started. SleepDelay: %v\n", EventSleepDelay.String()) - - for { - total, executed := GetEventCounter() - if total > 0 && executed != total { - for _, event := range Events { - if !event.Executed { - if verbose { - log.Debugf(log.EventMgr, "Events: Processing event %s.\n", event.String()) - } - success := event.CheckEventCondition(verbose) - if success { - msg := fmt.Sprintf( - "Events: ID: %d triggered on %s successfully [%v]\n", event.ID, - event.Exchange, event.String(), - ) - log.Infoln(log.EventMgr, msg) - comManager.PushEvent(base.Event{Type: "event", Message: msg}) - event.Executed = true - } - } - } - } - time.Sleep(EventSleepDelay) - } -} - -// IsValidExchange validates the exchange -func IsValidExchange(exchangeName string) bool { - cfg := config.GetConfig() - for x := range cfg.Exchanges { - if strings.EqualFold(cfg.Exchanges[x].Name, exchangeName) && cfg.Exchanges[x].Enabled { - return true - } - } - return false -} - -// IsValidCondition validates passed in condition -func IsValidCondition(condition string) bool { - switch condition { - case ConditionGreaterThan, ConditionGreaterThanOrEqual, ConditionLessThan, ConditionLessThanOrEqual, ConditionIsEqual: - return true - } - return false -} - -// IsValidAction validates passed in action -func IsValidAction(action string) bool { - action = strings.ToUpper(action) - switch action { - case ActionSMSNotify, ActionConsolePrint, ActionTest: - return true - } - return false -} - -// IsValidItem validates passed in Item -func IsValidItem(item string) bool { - item = strings.ToUpper(item) - switch item { - case ItemPrice, ItemOrderbook: - return true - } - return false -} diff --git a/engine/events_test.go b/engine/events_test.go deleted file mode 100644 index b6016524..00000000 --- a/engine/events_test.go +++ /dev/null @@ -1,336 +0,0 @@ -package engine - -import ( - "testing" - - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" -) - -const ( - testExchange = "Bitstamp" -) - -func addValidEvent() (int64, error) { - return Add(testExchange, - ItemPrice, - EventConditionParams{Condition: ConditionGreaterThan, Price: 1}, - currency.NewPair(currency.BTC, currency.USD), - asset.Spot, - "SMS,test") -} - -func TestAdd(t *testing.T) { - bot := CreateTestBot(t) - if config.Cfg.Name == "" && bot != nil { - config.Cfg = *bot.Config - } - _, err := Add("", "", EventConditionParams{}, currency.Pair{}, "", "") - if err == nil { - t.Error("should err on invalid params") - } - - _, err = addValidEvent() - if err != nil { - t.Error("unexpected result", err) - } - - _, err = addValidEvent() - if err != nil { - t.Error("unexpected result", err) - } - - if len(Events) != 2 { - t.Error("2 events should be stored") - } -} - -func TestRemove(t *testing.T) { - bot := CreateTestBot(t) - if config.Cfg.Name == "" && bot != nil { - config.Cfg = *bot.Config - } - id, err := addValidEvent() - if err != nil { - t.Error("unexpected result", err) - } - - if s := Remove(id); !s { - t.Error("unexpected result") - } - - if s := Remove(id); s { - t.Error("unexpected result") - } -} - -func TestGetEventCounter(t *testing.T) { - bot := CreateTestBot(t) - if config.Cfg.Name == "" && bot != nil { - config.Cfg = *bot.Config - } - _, err := addValidEvent() - if err != nil { - t.Error("unexpected result", err) - } - - n, e := GetEventCounter() - if n == 0 || e > 0 { - t.Error("unexpected result") - } - - Events[0].Executed = true - n, e = GetEventCounter() - if n == 0 || e == 0 { - t.Error("unexpected result") - } -} - -func TestExecuteAction(t *testing.T) { - t.Parallel() - bot := CreateTestBot(t) - if Bot == nil { - Bot = bot - } - if config.Cfg.Name == "" && bot != nil { - config.Cfg = *bot.Config - } - - var e Event - if r := e.ExecuteAction(); !r { - t.Error("unexpected result") - } - - e.Action = "SMS,test" - if r := e.ExecuteAction(); !r { - t.Error("unexpected result") - } - - e.Action = "SMS,ALL" - if r := e.ExecuteAction(); !r { - t.Error("unexpected result") - } -} - -func TestString(t *testing.T) { - t.Parallel() - e := Event{ - Exchange: testExchange, - Item: ItemPrice, - Condition: EventConditionParams{ - Condition: ConditionGreaterThan, - Price: 1, - }, - Pair: currency.NewPair(currency.BTC, currency.USD), - Asset: asset.Spot, - Action: "SMS,ALL", - } - - if r := e.String(); r != "If the BTCUSD [SPOT] PRICE on Bitstamp meets the following {> 1 false false 0} then SMS,ALL." { - t.Error("unexpected result") - } -} - -func TestProcessTicker(t *testing.T) { - e := Event{ - Exchange: testExchange, - Pair: currency.NewPair(currency.BTC, currency.USD), - Asset: asset.Spot, - Condition: EventConditionParams{ - Condition: ConditionGreaterThan, - Price: 1, - }, - } - - // now populate it with a 0 entry - tick := ticker.Price{ - Pair: currency.NewPair(currency.BTC, currency.USD), - ExchangeName: e.Exchange, - AssetType: e.Asset, - } - if err := ticker.ProcessTicker(&tick); err != nil { - t.Fatal("unexpected result:", err) - } - if r := e.processTicker(false); r { - t.Error("unexpected result") - } - - // now populate it with a number > 0 - tick.Last = 1337 - if err := ticker.ProcessTicker(&tick); err != nil { - t.Fatal("unexpected result:", err) - } - if r := e.processTicker(false); !r { - t.Error("unexpected result") - } -} - -func TestProcessCondition(t *testing.T) { - t.Parallel() - var e Event - tester := []struct { - Condition string - Actual float64 - Threshold float64 - ExpectedResult bool - }{ - {ConditionGreaterThan, 1, 2, false}, - {ConditionGreaterThan, 2, 1, true}, - {ConditionGreaterThanOrEqual, 1, 2, false}, - {ConditionGreaterThanOrEqual, 2, 1, true}, - {ConditionIsEqual, 1, 1, true}, - {ConditionIsEqual, 1, 2, false}, - {ConditionLessThan, 1, 2, true}, - {ConditionLessThan, 2, 1, false}, - {ConditionLessThanOrEqual, 1, 2, true}, - {ConditionLessThanOrEqual, 2, 1, false}, - } - for x := range tester { - e.Condition.Condition = tester[x].Condition - if r := e.processCondition(tester[x].Actual, tester[x].Threshold); r != tester[x].ExpectedResult { - t.Error("unexpected result") - } - } -} - -func TestProcessOrderbook(t *testing.T) { - e := Event{ - Exchange: testExchange, - Pair: currency.NewPair(currency.BTC, currency.USD), - Asset: asset.Spot, - Condition: EventConditionParams{ - Condition: ConditionGreaterThan, - CheckBidsAndAsks: true, - OrderbookAmount: 100, - }, - } - - // now populate it with a 0 entry - o := orderbook.Base{ - Pair: currency.NewPair(currency.BTC, currency.USD), - Bids: []orderbook.Item{{Amount: 24, Price: 23}}, - Asks: []orderbook.Item{{Amount: 24, Price: 23}}, - Exchange: e.Exchange, - Asset: e.Asset, - } - if err := o.Process(); err != nil { - t.Fatal("unexpected result:", err) - } - - if r := e.processOrderbook(false); !r { - t.Error("unexpected result") - } -} - -func TestCheckEventCondition(t *testing.T) { - t.Parallel() - if Bot == nil { - Bot = new(Engine) - } - - e := Event{ - Item: ItemPrice, - } - if r := e.CheckEventCondition(false); r { - t.Error("unexpected result") - } - - e.Item = ItemOrderbook - if r := e.CheckEventCondition(false); r { - t.Error("unexpected result") - } -} - -func TestIsValidEvent(t *testing.T) { - bot := CreateTestBot(t) - if config.Cfg.Name == "" && bot != nil { - config.Cfg = *bot.Config - } - // invalid exchange name - if err := IsValidEvent("meow", "", EventConditionParams{}, ""); err != errExchangeDisabled { - t.Error("unexpected result:", err) - } - - // invalid item - if err := IsValidEvent(testExchange, "", EventConditionParams{}, ""); err != errInvalidItem { - t.Error("unexpected result:", err) - } - - // invalid condition - if err := IsValidEvent(testExchange, ItemPrice, EventConditionParams{}, ""); err != errInvalidCondition { - t.Error("unexpected result:", err) - } - - // valid condition but empty price which will still throw an errInvalidCondition - c := EventConditionParams{ - Condition: ConditionGreaterThan, - } - if err := IsValidEvent(testExchange, ItemPrice, c, ""); err != errInvalidCondition { - t.Error("unexpected result:", err) - } - - // valid condition but empty orderbook amount will still still throw an errInvalidCondition - if err := IsValidEvent(testExchange, ItemOrderbook, c, ""); err != errInvalidCondition { - t.Error("unexpected result:", err) - } - - // test action splitting, but invalid - c.OrderbookAmount = 1337 - if err := IsValidEvent(testExchange, ItemOrderbook, c, "a,meow"); err != errInvalidAction { - t.Error("unexpected result:", err) - } - - // check for invalid action without splitting - if err := IsValidEvent(testExchange, ItemOrderbook, c, "hi"); err != errInvalidAction { - t.Error("unexpected result:", err) - } - - // valid event - if err := IsValidEvent(testExchange, ItemOrderbook, c, "SMS,test"); err != nil { - t.Error("unexpected result:", err) - } -} - -func TestIsValidExchange(t *testing.T) { - t.Parallel() - if s := IsValidExchange("invalidexchangerino"); s { - t.Error("unexpected result") - } - CreateTestBot(t) - if s := IsValidExchange(testExchange); !s { - t.Error("unexpected result") - } -} - -func TestIsValidCondition(t *testing.T) { - t.Parallel() - if s := IsValidCondition("invalidconditionerino"); s { - t.Error("unexpected result") - } - if s := IsValidCondition(ConditionGreaterThan); !s { - t.Error("unexpected result") - } -} - -func TestIsValidAction(t *testing.T) { - t.Parallel() - if s := IsValidAction("invalidactionerino"); s { - t.Error("unexpected result") - } - if s := IsValidAction(ActionSMSNotify); !s { - t.Error("unexpected result") - } -} - -func TestIsValidItem(t *testing.T) { - t.Parallel() - if s := IsValidItem("invaliditemerino"); s { - t.Error("unexpected result") - } - if s := IsValidItem(ItemPrice); !s { - t.Error("unexpected result") - } -} diff --git a/engine/exchange.go b/engine/exchange.go deleted file mode 100644 index 4d61f51c..00000000 --- a/engine/exchange.go +++ /dev/null @@ -1,406 +0,0 @@ -package engine - -import ( - "errors" - "strings" - "sync" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/binance" - "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" - "github.com/thrasher-corp/gocryptotrader/exchanges/bitflyer" - "github.com/thrasher-corp/gocryptotrader/exchanges/bithumb" - "github.com/thrasher-corp/gocryptotrader/exchanges/bitmex" - "github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp" - "github.com/thrasher-corp/gocryptotrader/exchanges/bittrex" - "github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets" - "github.com/thrasher-corp/gocryptotrader/exchanges/btse" - "github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro" - "github.com/thrasher-corp/gocryptotrader/exchanges/coinbene" - "github.com/thrasher-corp/gocryptotrader/exchanges/coinut" - "github.com/thrasher-corp/gocryptotrader/exchanges/exmo" - "github.com/thrasher-corp/gocryptotrader/exchanges/ftx" - "github.com/thrasher-corp/gocryptotrader/exchanges/gateio" - "github.com/thrasher-corp/gocryptotrader/exchanges/gemini" - "github.com/thrasher-corp/gocryptotrader/exchanges/hitbtc" - "github.com/thrasher-corp/gocryptotrader/exchanges/huobi" - "github.com/thrasher-corp/gocryptotrader/exchanges/itbit" - "github.com/thrasher-corp/gocryptotrader/exchanges/kraken" - "github.com/thrasher-corp/gocryptotrader/exchanges/lakebtc" - "github.com/thrasher-corp/gocryptotrader/exchanges/lbank" - "github.com/thrasher-corp/gocryptotrader/exchanges/localbitcoins" - "github.com/thrasher-corp/gocryptotrader/exchanges/okcoin" - "github.com/thrasher-corp/gocryptotrader/exchanges/okex" - "github.com/thrasher-corp/gocryptotrader/exchanges/poloniex" - "github.com/thrasher-corp/gocryptotrader/exchanges/yobit" - "github.com/thrasher-corp/gocryptotrader/exchanges/zb" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// vars related to exchange functions -var ( - ErrNoExchangesLoaded = errors.New("no exchanges have been loaded") - ErrExchangeNotFound = errors.New("exchange not found") - ErrExchangeAlreadyLoaded = errors.New("exchange already loaded") - ErrExchangeFailedToLoad = errors.New("exchange failed to load") -) - -type exchangeManager struct { - m sync.Mutex - exchanges map[string]exchange.IBotExchange -} - -func (bot *Engine) dryrunParamInteraction(param string) { - if !bot.Settings.CheckParamInteraction { - return - } - - if !bot.Settings.EnableDryRun { - log.Warnf(log.Global, - "Command line argument '-%s' induces dry run mode."+ - " Set -dryrun=false if you wish to override this.", - param) - bot.Settings.EnableDryRun = true - } -} - -func (e *exchangeManager) add(exch exchange.IBotExchange) { - e.m.Lock() - if e.exchanges == nil { - e.exchanges = make(map[string]exchange.IBotExchange) - } - e.exchanges[strings.ToLower(exch.GetName())] = exch - e.m.Unlock() -} - -func (e *exchangeManager) getExchanges() []exchange.IBotExchange { - if e.Len() == 0 { - return nil - } - - e.m.Lock() - defer e.m.Unlock() - var exchs []exchange.IBotExchange - for x := range e.exchanges { - exchs = append(exchs, e.exchanges[x]) - } - return exchs -} - -func (e *exchangeManager) removeExchange(exchName string) error { - if e.Len() == 0 { - return ErrNoExchangesLoaded - } - exch := e.getExchangeByName(exchName) - if exch == nil { - return ErrExchangeNotFound - } - e.m.Lock() - defer e.m.Unlock() - delete(e.exchanges, strings.ToLower(exchName)) - log.Infof(log.ExchangeSys, "%s exchange unloaded successfully.\n", exchName) - return nil -} - -func (e *exchangeManager) getExchangeByName(exchangeName string) exchange.IBotExchange { - if e.Len() == 0 { - return nil - } - e.m.Lock() - defer e.m.Unlock() - exch, ok := e.exchanges[strings.ToLower(exchangeName)] - if !ok { - return nil - } - return exch -} - -func (e *exchangeManager) Len() int { - e.m.Lock() - defer e.m.Unlock() - return len(e.exchanges) -} - -// GetExchangeByName returns an exchange given an exchange name -func (bot *Engine) GetExchangeByName(exchName string) exchange.IBotExchange { - return bot.exchangeManager.getExchangeByName(exchName) -} - -// UnloadExchange unloads an exchange by name -func (bot *Engine) UnloadExchange(exchName string) error { - exchCfg, err := bot.Config.GetExchangeConfig(exchName) - if err != nil { - return err - } - - err = bot.exchangeManager.removeExchange(exchName) - if err != nil { - return err - } - - exchCfg.Enabled = false - return nil -} - -// GetExchanges retrieves the loaded exchanges -func (bot *Engine) GetExchanges() []exchange.IBotExchange { - return bot.exchangeManager.getExchanges() -} - -// LoadExchange loads an exchange by name -func (bot *Engine) LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { - nameLower := strings.ToLower(name) - var exch exchange.IBotExchange - - if bot.exchangeManager.getExchangeByName(nameLower) != nil { - return ErrExchangeAlreadyLoaded - } - - switch nameLower { - case "binance": - exch = new(binance.Binance) - case "bitfinex": - exch = new(bitfinex.Bitfinex) - case "bitflyer": - exch = new(bitflyer.Bitflyer) - case "bithumb": - exch = new(bithumb.Bithumb) - case "bitmex": - exch = new(bitmex.Bitmex) - case "bitstamp": - exch = new(bitstamp.Bitstamp) - case "bittrex": - exch = new(bittrex.Bittrex) - case "btc markets": - exch = new(btcmarkets.BTCMarkets) - case "btse": - exch = new(btse.BTSE) - case "coinbene": - exch = new(coinbene.Coinbene) - case "coinut": - exch = new(coinut.COINUT) - case "exmo": - exch = new(exmo.EXMO) - case "coinbasepro": - exch = new(coinbasepro.CoinbasePro) - case "ftx": - exch = new(ftx.FTX) - case "gateio": - exch = new(gateio.Gateio) - case "gemini": - exch = new(gemini.Gemini) - case "hitbtc": - exch = new(hitbtc.HitBTC) - case "huobi": - exch = new(huobi.HUOBI) - case "itbit": - exch = new(itbit.ItBit) - case "kraken": - exch = new(kraken.Kraken) - case "lakebtc": - exch = new(lakebtc.LakeBTC) - case "lbank": - exch = new(lbank.Lbank) - case "localbitcoins": - exch = new(localbitcoins.LocalBitcoins) - case "okcoin international": - exch = new(okcoin.OKCoin) - case "okex": - exch = new(okex.OKEX) - case "poloniex": - exch = new(poloniex.Poloniex) - case "yobit": - exch = new(yobit.Yobit) - case "zb": - exch = new(zb.ZB) - default: - return ErrExchangeNotFound - } - - if exch == nil { - return ErrExchangeFailedToLoad - } - - var localWG sync.WaitGroup - localWG.Add(1) - go func() { - exch.SetDefaults() - localWG.Done() - }() - exchCfg, err := bot.Config.GetExchangeConfig(name) - if err != nil { - return err - } - - if bot.Settings.EnableAllPairs && - exchCfg.CurrencyPairs != nil { - assets := exchCfg.CurrencyPairs.GetAssetTypes() - for x := range assets { - var pairs currency.Pairs - pairs, err = exchCfg.CurrencyPairs.GetPairs(assets[x], false) - if err != nil { - return err - } - exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true) - } - } - - if bot.Settings.EnableExchangeVerbose { - exchCfg.Verbose = true - } - if exchCfg.Features != nil { - if bot.Settings.EnableExchangeWebsocketSupport && - exchCfg.Features.Supports.Websocket { - exchCfg.Features.Enabled.Websocket = true - } - if bot.Settings.EnableExchangeAutoPairUpdates && - exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates { - exchCfg.Features.Enabled.AutoPairUpdates = true - } - if bot.Settings.DisableExchangeAutoPairUpdates { - if exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates { - exchCfg.Features.Enabled.AutoPairUpdates = false - } - } - } - if bot.Settings.HTTPUserAgent != "" { - exchCfg.HTTPUserAgent = bot.Settings.HTTPUserAgent - } - if bot.Settings.HTTPProxy != "" { - exchCfg.ProxyAddress = bot.Settings.HTTPProxy - } - if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout { - exchCfg.HTTPTimeout = bot.Settings.HTTPTimeout - } - if bot.Settings.EnableExchangeHTTPDebugging { - exchCfg.HTTPDebugging = bot.Settings.EnableExchangeHTTPDebugging - } - - localWG.Wait() - if !bot.Settings.EnableExchangeHTTPRateLimiter { - log.Warnf(log.ExchangeSys, - "Loaded exchange %s rate limiting has been turned off.\n", - exch.GetName(), - ) - err = exch.DisableRateLimiter() - if err != nil { - log.Errorf(log.ExchangeSys, - "Loaded exchange %s rate limiting cannot be turned off: %s.\n", - exch.GetName(), - err, - ) - } - } - - exchCfg.Enabled = true - err = exch.Setup(exchCfg) - if err != nil { - exchCfg.Enabled = false - return err - } - - bot.exchangeManager.add(exch) - base := exch.GetBase() - if base.API.AuthenticatedSupport || - base.API.AuthenticatedWebsocketSupport { - assetTypes := base.GetAssetTypes() - var useAsset asset.Item - for a := range assetTypes { - err = base.CurrencyPairs.IsAssetEnabled(assetTypes[a]) - if err != nil { - continue - } - useAsset = assetTypes[a] - break - } - err = exch.ValidateCredentials(useAsset) - if err != nil { - log.Warnf(log.ExchangeSys, - "%s: Cannot validate credentials, authenticated support has been disabled, Error: %s\n", - base.Name, - err) - base.API.AuthenticatedSupport = false - base.API.AuthenticatedWebsocketSupport = false - exchCfg.API.AuthenticatedSupport = false - exchCfg.API.AuthenticatedWebsocketSupport = false - } - } - - if useWG { - exch.Start(wg) - } else { - tempWG := sync.WaitGroup{} - exch.Start(&tempWG) - tempWG.Wait() - } - - return nil -} - -// SetupExchanges sets up the exchanges used by the Bot -func (bot *Engine) SetupExchanges() error { - var wg sync.WaitGroup - configs := bot.Config.GetAllExchangeConfigs() - if bot.Settings.EnableAllPairs { - bot.dryrunParamInteraction("enableallpairs") - } - if bot.Settings.EnableAllExchanges { - bot.dryrunParamInteraction("enableallexchanges") - } - if bot.Settings.EnableExchangeVerbose { - bot.dryrunParamInteraction("exchangeverbose") - } - if bot.Settings.EnableExchangeWebsocketSupport { - bot.dryrunParamInteraction("exchangewebsocketsupport") - } - if bot.Settings.EnableExchangeAutoPairUpdates { - bot.dryrunParamInteraction("exchangeautopairupdates") - } - if bot.Settings.DisableExchangeAutoPairUpdates { - bot.dryrunParamInteraction("exchangedisableautopairupdates") - } - if bot.Settings.HTTPUserAgent != "" { - bot.dryrunParamInteraction("httpuseragent") - } - if bot.Settings.HTTPProxy != "" { - bot.dryrunParamInteraction("httpproxy") - } - if bot.Settings.HTTPTimeout != exchange.DefaultHTTPTimeout { - bot.dryrunParamInteraction("httptimeout") - } - if bot.Settings.EnableExchangeHTTPDebugging { - bot.dryrunParamInteraction("exchangehttpdebugging") - } - - for x := range configs { - if !configs[x].Enabled && !bot.Settings.EnableAllExchanges { - log.Debugf(log.ExchangeSys, "%s: Exchange support: Disabled\n", configs[x].Name) - continue - } - wg.Add(1) - cfg := configs[x] - go func(currCfg config.ExchangeConfig) { - defer wg.Done() - err := bot.LoadExchange(currCfg.Name, true, &wg) - if err != nil { - log.Errorf(log.ExchangeSys, "LoadExchange %s failed: %s\n", currCfg.Name, err) - return - } - log.Debugf(log.ExchangeSys, - "%s: Exchange support: Enabled (Authenticated API support: %s - Verbose mode: %s).\n", - currCfg.Name, - common.IsEnabled(currCfg.API.AuthenticatedSupport), - common.IsEnabled(currCfg.Verbose), - ) - }(cfg) - } - wg.Wait() - if len(bot.exchangeManager.exchanges) == 0 { - return errors.New("no exchanges are loaded") - } - return nil -} diff --git a/engine/exchange_manager.go b/engine/exchange_manager.go new file mode 100644 index 00000000..6c843584 --- /dev/null +++ b/engine/exchange_manager.go @@ -0,0 +1,193 @@ +package engine + +import ( + "errors" + "fmt" + "strings" + "sync" + + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/binance" + "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" + "github.com/thrasher-corp/gocryptotrader/exchanges/bitflyer" + "github.com/thrasher-corp/gocryptotrader/exchanges/bithumb" + "github.com/thrasher-corp/gocryptotrader/exchanges/bitmex" + "github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp" + "github.com/thrasher-corp/gocryptotrader/exchanges/bittrex" + "github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets" + "github.com/thrasher-corp/gocryptotrader/exchanges/btse" + "github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro" + "github.com/thrasher-corp/gocryptotrader/exchanges/coinbene" + "github.com/thrasher-corp/gocryptotrader/exchanges/coinut" + "github.com/thrasher-corp/gocryptotrader/exchanges/exmo" + "github.com/thrasher-corp/gocryptotrader/exchanges/ftx" + "github.com/thrasher-corp/gocryptotrader/exchanges/gateio" + "github.com/thrasher-corp/gocryptotrader/exchanges/gemini" + "github.com/thrasher-corp/gocryptotrader/exchanges/hitbtc" + "github.com/thrasher-corp/gocryptotrader/exchanges/huobi" + "github.com/thrasher-corp/gocryptotrader/exchanges/itbit" + "github.com/thrasher-corp/gocryptotrader/exchanges/kraken" + "github.com/thrasher-corp/gocryptotrader/exchanges/lakebtc" + "github.com/thrasher-corp/gocryptotrader/exchanges/lbank" + "github.com/thrasher-corp/gocryptotrader/exchanges/localbitcoins" + "github.com/thrasher-corp/gocryptotrader/exchanges/okcoin" + "github.com/thrasher-corp/gocryptotrader/exchanges/okex" + "github.com/thrasher-corp/gocryptotrader/exchanges/poloniex" + "github.com/thrasher-corp/gocryptotrader/exchanges/yobit" + "github.com/thrasher-corp/gocryptotrader/exchanges/zb" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// vars related to exchange functions +var ( + ErrNoExchangesLoaded = errors.New("no exchanges have been loaded") + ErrExchangeNotFound = errors.New("exchange not found") + ErrExchangeAlreadyLoaded = errors.New("exchange already loaded") + ErrExchangeFailedToLoad = errors.New("exchange failed to load") +) + +// ExchangeManager manages what exchanges are loaded +type ExchangeManager struct { + m sync.Mutex + exchanges map[string]exchange.IBotExchange +} + +// SetupExchangeManager creates a new exchange manager +func SetupExchangeManager() *ExchangeManager { + return &ExchangeManager{ + exchanges: make(map[string]exchange.IBotExchange), + } +} + +// Add adds or replaces an exchange +func (m *ExchangeManager) Add(exch exchange.IBotExchange) { + if exch == nil { + return + } + m.m.Lock() + m.exchanges[strings.ToLower(exch.GetName())] = exch + m.m.Unlock() +} + +// GetExchanges returns all stored exchanges +func (m *ExchangeManager) GetExchanges() []exchange.IBotExchange { + m.m.Lock() + defer m.m.Unlock() + var exchs []exchange.IBotExchange + for _, x := range m.exchanges { + exchs = append(exchs, x) + } + return exchs +} + +// RemoveExchange removes an exchange from the manager +func (m *ExchangeManager) RemoveExchange(exchName string) error { + if m.Len() == 0 { + return ErrNoExchangesLoaded + } + exch := m.GetExchangeByName(exchName) + if exch == nil { + return ErrExchangeNotFound + } + m.m.Lock() + defer m.m.Unlock() + delete(m.exchanges, strings.ToLower(exchName)) + log.Infof(log.ExchangeSys, "%s exchange unloaded successfully.\n", exchName) + return nil +} + +// GetExchangeByName returns an exchange by its name if it exists +func (m *ExchangeManager) GetExchangeByName(exchangeName string) exchange.IBotExchange { + if m == nil { + return nil + } + m.m.Lock() + defer m.m.Unlock() + exch, ok := m.exchanges[strings.ToLower(exchangeName)] + if !ok { + return nil + } + return exch +} + +// Len says how many exchanges are loaded +func (m *ExchangeManager) Len() int { + m.m.Lock() + defer m.m.Unlock() + return len(m.exchanges) +} + +// NewExchangeByName helps create a new exchange to be loaded +func (m *ExchangeManager) NewExchangeByName(name string) (exchange.IBotExchange, error) { + if m == nil { + return nil, fmt.Errorf("exchange manager %w", ErrNilSubsystem) + } + nameLower := strings.ToLower(name) + if m.GetExchangeByName(nameLower) != nil { + return nil, fmt.Errorf("%s %w", name, ErrExchangeAlreadyLoaded) + } + var exch exchange.IBotExchange + + switch nameLower { + case "binance": + exch = new(binance.Binance) + case "bitfinex": + exch = new(bitfinex.Bitfinex) + case "bitflyer": + exch = new(bitflyer.Bitflyer) + case "bithumb": + exch = new(bithumb.Bithumb) + case "bitmex": + exch = new(bitmex.Bitmex) + case "bitstamp": + exch = new(bitstamp.Bitstamp) + case "bittrex": + exch = new(bittrex.Bittrex) + case "btc markets": + exch = new(btcmarkets.BTCMarkets) + case "btse": + exch = new(btse.BTSE) + case "coinbene": + exch = new(coinbene.Coinbene) + case "coinut": + exch = new(coinut.COINUT) + case "exmo": + exch = new(exmo.EXMO) + case "coinbasepro": + exch = new(coinbasepro.CoinbasePro) + case "ftx": + exch = new(ftx.FTX) + case "gateio": + exch = new(gateio.Gateio) + case "gemini": + exch = new(gemini.Gemini) + case "hitbtc": + exch = new(hitbtc.HitBTC) + case "huobi": + exch = new(huobi.HUOBI) + case "itbit": + exch = new(itbit.ItBit) + case "kraken": + exch = new(kraken.Kraken) + case "lakebtc": + exch = new(lakebtc.LakeBTC) + case "lbank": + exch = new(lbank.Lbank) + case "localbitcoins": + exch = new(localbitcoins.LocalBitcoins) + case "okcoin international": + exch = new(okcoin.OKCoin) + case "okex": + exch = new(okex.OKEX) + case "poloniex": + exch = new(poloniex.Poloniex) + case "yobit": + exch = new(yobit.Yobit) + case "zb": + exch = new(zb.ZB) + default: + return nil, fmt.Errorf("%s, %w", nameLower, ErrExchangeNotFound) + } + + return exch, nil +} diff --git a/engine/exchange_manager.md b/engine/exchange_manager.md new file mode 100644 index 00000000..b92ecf3f --- /dev/null +++ b/engine/exchange_manager.md @@ -0,0 +1,45 @@ +# GoCryptoTrader package Exchange_manager + + + + +[![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/exchange_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This exchange_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Exchange_manager ++ The exchange manager subsystem is used load and store exchanges so that the engine Bot can use them to track orderbooks, submit orders etc etc ++ The exchange manager itself is not customisable, it is always enabled. ++ The exchange manager by default will load all exchanges that are enabled in your config, however, it will also load exchanges by request via GRPC commands + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/exchange_manager_test.go b/engine/exchange_manager_test.go new file mode 100644 index 00000000..86ac26be --- /dev/null +++ b/engine/exchange_manager_test.go @@ -0,0 +1,81 @@ +package engine + +import ( + "strings" + "testing" + + "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" +) + +func TestSetupExchangeManager(t *testing.T) { + t.Parallel() + m := SetupExchangeManager() + if m == nil { + t.Fatalf("unexpected response") + } + if m.exchanges == nil { + t.Error("unexpected response") + } +} + +func TestExchangeManagerAdd(t *testing.T) { + t.Parallel() + m := SetupExchangeManager() + b := new(bitfinex.Bitfinex) + b.SetDefaults() + m.Add(b) + if exch := m.GetExchanges(); exch[0].GetName() != "Bitfinex" { + t.Error("unexpected exchange name") + } +} + +func TestExchangeManagerGetExchanges(t *testing.T) { + t.Parallel() + m := SetupExchangeManager() + if exchanges := m.GetExchanges(); exchanges != nil { + t.Error("unexpected value") + } + b := new(bitfinex.Bitfinex) + b.SetDefaults() + m.Add(b) + if exch := m.GetExchanges(); exch[0].GetName() != "Bitfinex" { + t.Error("unexpected exchange name") + } +} + +func TestExchangeManagerRemoveExchange(t *testing.T) { + t.Parallel() + m := SetupExchangeManager() + if err := m.RemoveExchange("Bitfinex"); err != ErrNoExchangesLoaded { + t.Error("no exchanges should be loaded") + } + b := new(bitfinex.Bitfinex) + b.SetDefaults() + m.Add(b) + if err := m.RemoveExchange("Bitstamp"); err != ErrExchangeNotFound { + t.Error("Bitstamp exchange should return an error") + } + if err := m.RemoveExchange("BiTFiNeX"); err != nil { + t.Error("exchange should have been removed") + } + if m.Len() != 0 { + t.Error("exchange manager len should be 0") + } +} + +func TestNewExchangeByName(t *testing.T) { + m := SetupExchangeManager() + exchanges := []string{"binance", "bitfinex", "bitflyer", "bithumb", "bitmex", "bitstamp", "bittrex", "btc markets", "btse", "coinbene", "coinut", "exmo", "coinbasepro", "ftx", "gateio", "gemini", "hitbtc", "huobi", "itbit", "kraken", "lakebtc", "lbank", "localbitcoins", "okcoin international", "okex", "poloniex", "yobit", "zb", "fake"} + for i := range exchanges { + exch, err := m.NewExchangeByName(exchanges[i]) + if err != nil && exchanges[i] != "fake" { + t.Error(err) + } + if err == nil { + exch.SetDefaults() + if !strings.EqualFold(exch.GetName(), exchanges[i]) { + t.Error("did not load expected exchange") + } + } + } +} diff --git a/engine/exchange_test.go b/engine/exchange_test.go deleted file mode 100644 index 53a8cfe7..00000000 --- a/engine/exchange_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package engine - -import ( - "testing" - - "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" -) - -func TestExchangeManagerAdd(t *testing.T) { - t.Parallel() - var e exchangeManager - b := new(bitfinex.Bitfinex) - b.SetDefaults() - e.add(b) - if exch := e.getExchanges(); exch[0].GetName() != "Bitfinex" { - t.Error("unexpected exchange name") - } -} - -func TestExchangeManagerGetExchanges(t *testing.T) { - t.Parallel() - var e exchangeManager - if exchanges := e.getExchanges(); exchanges != nil { - t.Error("unexpected value") - } - b := new(bitfinex.Bitfinex) - b.SetDefaults() - e.add(b) - if exch := e.getExchanges(); exch[0].GetName() != "Bitfinex" { - t.Error("unexpected exchange name") - } -} - -func TestExchangeManagerRemoveExchange(t *testing.T) { - t.Parallel() - var e exchangeManager - if err := e.removeExchange("Bitfinex"); err != ErrNoExchangesLoaded { - t.Error("no exchanges should be loaded") - } - b := new(bitfinex.Bitfinex) - b.SetDefaults() - e.add(b) - if err := e.removeExchange(testExchange); err != ErrExchangeNotFound { - t.Error("Bitstamp exchange should return an error") - } - if err := e.removeExchange("BiTFiNeX"); err != nil { - t.Error("exchange should have been removed") - } - if e.Len() != 0 { - t.Error("exchange manager len should be 0") - } -} - -func TestCheckExchangeExists(t *testing.T) { - e := CreateTestBot(t) - - if e.GetExchangeByName(testExchange) == nil { - t.Errorf("TestGetExchangeExists: Unable to find exchange") - } - - if e.GetExchangeByName("Asdsad") != nil { - t.Errorf("TestGetExchangeExists: Non-existent exchange found") - } -} - -func TestGetExchangeByName(t *testing.T) { - e := CreateTestBot(t) - - exch := e.GetExchangeByName(testExchange) - if exch == nil { - t.Errorf("TestGetExchangeByName: Failed to get exchange") - } - - if !exch.IsEnabled() { - t.Errorf("TestGetExchangeByName: Unexpected result") - } - - exch.SetEnabled(false) - bfx := e.GetExchangeByName(testExchange) - if bfx.IsEnabled() { - t.Errorf("TestGetExchangeByName: Unexpected result") - } - - if exch.GetName() != testExchange { - t.Errorf("TestGetExchangeByName: Unexpected result") - } - - exch = e.GetExchangeByName("Asdasd") - if exch != nil { - t.Errorf("TestGetExchangeByName: Non-existent exchange found") - } -} - -func TestUnloadExchange(t *testing.T) { - e := CreateTestBot(t) - - err := e.UnloadExchange("asdf") - if err == nil || err.Error() != "exchange asdf not found" { - t.Errorf("TestUnloadExchange: Incorrect result: %s", - err) - } - - err = e.UnloadExchange(testExchange) - if err != nil { - t.Errorf("TestUnloadExchange: Failed to get exchange. %s", - err) - } - - err = e.UnloadExchange(fakePassExchange) - if err != nil { - t.Errorf("TestUnloadExchange: Failed to unload exchange. %s", - err) - } - - err = e.UnloadExchange(testExchange) - if err != ErrNoExchangesLoaded { - t.Errorf("TestUnloadExchange: Incorrect result: %s", - err) - } -} - -func TestDryRunParamInteraction(t *testing.T) { - bot := CreateTestBot(t) - - // Simulate overiding default settings and ensure that enabling exchange - // verbose mode will be set on Bitfinex - var err error - if err = bot.UnloadExchange(testExchange); err != nil { - t.Error(err) - } - - bot.Settings.CheckParamInteraction = false - bot.Settings.EnableExchangeVerbose = false - if err = bot.LoadExchange(testExchange, false, nil); err != nil { - t.Error(err) - } - - exchCfg, err := bot.Config.GetExchangeConfig(testExchange) - if err != nil { - t.Error(err) - } - - if exchCfg.Verbose { - t.Error("verbose should have been disabled") - } - - if err = bot.UnloadExchange(testExchange); err != nil { - t.Error(err) - } - - // Now set dryrun mode to true, - // enable exchange verbose mode and verify that verbose mode - // will be set on Bitfinex - bot.Settings.EnableDryRun = true - bot.Settings.CheckParamInteraction = true - bot.Settings.EnableExchangeVerbose = true - if err = bot.LoadExchange(testExchange, false, nil); err != nil { - t.Error(err) - } - - exchCfg, err = bot.Config.GetExchangeConfig(testExchange) - if err != nil { - t.Error(err) - } - - if !bot.Settings.EnableDryRun || - !exchCfg.Verbose { - t.Error("dryrun should be true and verbose should be true") - } -} diff --git a/engine/fake_exchange_test.go b/engine/fake_exchange_test.go deleted file mode 100644 index 780d9081..00000000 --- a/engine/fake_exchange_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package engine - -import ( - "sync" - "time" - - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/trade" - "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" -) - -const ( - fakePassExchange = "FakePassExchange" -) - -// FakePassingExchange is used to override IBotExchange responses in tests -// In this context, we don't care what FakePassingExchange does as we're testing -// the engine package -type FakePassingExchange struct { - exchange.Base -} - -// addPassingFakeExchange adds an exchange to engine tests where all funcs return a positive result -func addPassingFakeExchange(baseExchangeName string, bot *Engine) error { - testExch := bot.GetExchangeByName(baseExchangeName) - if testExch == nil { - return ErrExchangeNotFound - } - - base := testExch.GetBase() - bot.Config.Exchanges = append(bot.Config.Exchanges, config.ExchangeConfig{ - Name: fakePassExchange, - Enabled: true, - Verbose: false, - }) - b := true - var pairStoreData = currency.PairStore{ - AssetEnabled: &b, - } - var currencyMap = make(map[asset.Item]*currency.PairStore) - currencyMap[asset.Spot] = &pairStoreData - - bot.exchangeManager.add(&FakePassingExchange{ - Base: exchange.Base{ - Name: fakePassExchange, - CurrencyPairs: currency.PairsManager{ - Pairs: currencyMap}, - Enabled: true, - LoadedByConfig: true, - SkipAuthCheck: true, - API: base.API, - Features: base.Features, - HTTPTimeout: base.HTTPTimeout, - HTTPUserAgent: base.HTTPUserAgent, - HTTPRecording: base.HTTPRecording, - HTTPDebugging: base.HTTPDebugging, - WebsocketResponseCheckTimeout: base.WebsocketResponseCheckTimeout, - WebsocketResponseMaxLimit: base.WebsocketResponseMaxLimit, - WebsocketOrderbookBufferLimit: base.WebsocketOrderbookBufferLimit, - Websocket: base.Websocket, - Requester: base.Requester, - Config: base.Config, - }, - }) - return nil -} - -func (h *FakePassingExchange) Setup(_ *config.ExchangeConfig) error { return nil } -func (h *FakePassingExchange) Start(_ *sync.WaitGroup) {} -func (h *FakePassingExchange) SetDefaults() {} -func (h *FakePassingExchange) GetName() string { return fakePassExchange } -func (h *FakePassingExchange) IsEnabled() bool { return true } -func (h *FakePassingExchange) SetEnabled(bool) {} -func (h *FakePassingExchange) ValidateCredentials(_ asset.Item) error { return nil } - -func (h *FakePassingExchange) FetchTicker(_ currency.Pair, _ asset.Item) (*ticker.Price, error) { - return nil, nil -} -func (h *FakePassingExchange) UpdateTicker(_ currency.Pair, _ asset.Item) (*ticker.Price, error) { - return nil, nil -} -func (h *FakePassingExchange) FetchOrderbook(_ currency.Pair, _ asset.Item) (*orderbook.Base, error) { - return nil, nil -} -func (h *FakePassingExchange) UpdateOrderbook(_ currency.Pair, _ asset.Item) (*orderbook.Base, error) { - return nil, nil -} -func (h *FakePassingExchange) FetchTradablePairs(_ asset.Item) ([]string, error) { - return nil, nil -} -func (h *FakePassingExchange) UpdateTradablePairs(_ bool) error { return nil } - -func (h *FakePassingExchange) GetEnabledPairs(_ asset.Item) (currency.Pairs, error) { - return currency.Pairs{}, nil -} -func (h *FakePassingExchange) GetAvailablePairs(_ asset.Item) (currency.Pairs, error) { - return currency.Pairs{}, nil -} - -func (h *FakePassingExchange) FetchAccountInfo(_ asset.Item) (account.Holdings, error) { - return account.Holdings{ - Exchange: h.Name, - Accounts: []account.SubAccount{ - { - Currencies: []account.Balance{ - { - CurrencyName: currency.BTC, - TotalValue: 10., - Hold: 0, - }, - }, - }, - }, - }, nil -} - -func (h *FakePassingExchange) UpdateAccountInfo(_ asset.Item) (account.Holdings, error) { - return account.Holdings{ - Exchange: h.Name, - Accounts: []account.SubAccount{ - { - Currencies: []account.Balance{ - { - CurrencyName: currency.BTC, - TotalValue: 20., - Hold: 0, - }, - }, - }, - }, - }, nil -} - -func (h *FakePassingExchange) GetAuthenticatedAPISupport(_ uint8) bool { return true } -func (h *FakePassingExchange) SetPairs(_ currency.Pairs, _ asset.Item, _ bool) error { - return nil -} -func (h *FakePassingExchange) GetAssetTypes() asset.Items { return asset.Items{asset.Spot} } -func (h *FakePassingExchange) GetHistoricTrades(_ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { - return nil, nil -} -func (h *FakePassingExchange) GetRecentTrades(_ currency.Pair, _ asset.Item) ([]trade.Data, error) { - return nil, nil -} -func (h *FakePassingExchange) SupportsAutoPairUpdates() bool { return true } -func (h *FakePassingExchange) SupportsRESTTickerBatchUpdates() bool { return true } -func (h *FakePassingExchange) GetFeeByType(_ *exchange.FeeBuilder) (float64, error) { - return 0, nil -} -func (h *FakePassingExchange) GetLastPairsUpdateTime() int64 { return 0 } -func (h *FakePassingExchange) GetWithdrawPermissions() uint32 { return 0 } -func (h *FakePassingExchange) FormatWithdrawPermissions() string { return "" } -func (h *FakePassingExchange) SupportsWithdrawPermissions(_ uint32) bool { return true } -func (h *FakePassingExchange) GetFundingHistory() ([]exchange.FundHistory, error) { return nil, nil } -func (h *FakePassingExchange) SubmitOrder(_ *order.Submit) (order.SubmitResponse, error) { - return order.SubmitResponse{ - IsOrderPlaced: true, - FullyMatched: true, - OrderID: "FakePassingExchangeOrder", - }, nil -} -func (h *FakePassingExchange) ModifyOrder(_ *order.Modify) (string, error) { return "", nil } -func (h *FakePassingExchange) CancelOrder(_ *order.Cancel) error { return nil } -func (h *FakePassingExchange) CancelBatchOrders(_ []order.Cancel) (order.CancelBatchResponse, error) { - return order.CancelBatchResponse{}, nil -} -func (h *FakePassingExchange) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { - return order.CancelAllResponse{}, nil -} -func (h *FakePassingExchange) GetOrderInfo(_ string, _ currency.Pair, _ asset.Item) (order.Detail, error) { - return order.Detail{ - Exchange: fakePassExchange, - ID: "fakeOrder", - }, nil -} -func (h *FakePassingExchange) GetWithdrawalsHistory(_ currency.Code) ([]exchange.WithdrawalHistory, error) { - return nil, nil -} -func (h *FakePassingExchange) GetDepositAddress(_ currency.Code, _ string) (string, error) { - return "", nil -} -func (h *FakePassingExchange) GetOrderHistory(_ *order.GetOrdersRequest) ([]order.Detail, error) { - return nil, nil -} -func (h *FakePassingExchange) GetActiveOrders(_ *order.GetOrdersRequest) ([]order.Detail, error) { - pair, err := currency.NewPairFromString("BTCUSD") - if err != nil { - return nil, err - } - - return []order.Detail{ - { - Price: 1337, - Amount: 1337, - Exchange: fakePassExchange, - ID: "fakeOrder", - Type: order.Market, - Side: order.Buy, - Status: order.Active, - AssetType: asset.Spot, - Date: time.Now(), - Pair: pair, - }, - }, nil -} -func (h *FakePassingExchange) SetHTTPClientUserAgent(_ string) {} -func (h *FakePassingExchange) GetHTTPClientUserAgent() string { return "" } -func (h *FakePassingExchange) SetClientProxyAddress(_ string) error { return nil } -func (h *FakePassingExchange) SupportsWebsocket() bool { return true } -func (h *FakePassingExchange) SupportsREST() bool { return true } -func (h *FakePassingExchange) IsWebsocketEnabled() bool { return true } -func (h *FakePassingExchange) GetWebsocket() (*stream.Websocket, error) { return nil, nil } -func (h *FakePassingExchange) SubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error { - return nil -} -func (h *FakePassingExchange) UnsubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error { - return nil -} -func (h *FakePassingExchange) AuthenticateWebsocket() error { return nil } -func (h *FakePassingExchange) GetSubscriptions() ([]stream.ChannelSubscription, error) { - return nil, nil -} -func (h *FakePassingExchange) GetDefaultConfig() (*config.ExchangeConfig, error) { return nil, nil } -func (h *FakePassingExchange) SupportsAsset(_ asset.Item) bool { return true } -func (h *FakePassingExchange) GetHistoricCandles(_ currency.Pair, _ asset.Item, _, _ time.Time, _ kline.Interval) (kline.Item, error) { - return kline.Item{}, nil -} -func (h *FakePassingExchange) GetHistoricCandlesExtended(_ currency.Pair, _ asset.Item, _, _ time.Time, _ kline.Interval) (kline.Item, error) { - return kline.Item{}, nil -} -func (h *FakePassingExchange) DisableRateLimiter() error { return nil } -func (h *FakePassingExchange) EnableRateLimiter() error { return nil } -func (h *FakePassingExchange) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, nil -} -func (h *FakePassingExchange) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, nil -} -func (h *FakePassingExchange) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, nil -} diff --git a/engine/helpers.go b/engine/helpers.go index f16f8e89..b03a7431 100644 --- a/engine/helpers.go +++ b/engine/helpers.go @@ -20,6 +20,7 @@ import ( "github.com/pquerna/otp/totp" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/file" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -28,8 +29,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/stats" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/gctscript/vm" "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/portfolio" ) var ( @@ -41,19 +42,19 @@ var ( // GetSubsystemsStatus returns the status of various subsystems func (bot *Engine) GetSubsystemsStatus() map[string]bool { systems := make(map[string]bool) - systems["communications"] = bot.CommsManager.Started() - systems["internet_monitor"] = bot.ConnectionManager.Started() - systems["orders"] = bot.OrderManager.Started() - systems["portfolio"] = bot.PortfolioManager.Started() - systems["ntp_timekeeper"] = bot.NTPManager.Started() - systems["database"] = bot.DatabaseManager.Started() - systems["exchange_syncer"] = bot.Settings.EnableExchangeSyncManager - systems["grpc"] = bot.Settings.EnableGRPC - systems["grpc_proxy"] = bot.Settings.EnableGRPCProxy - systems["gctscript"] = bot.GctScriptManager.Started() - systems["deprecated_rpc"] = bot.Settings.EnableDeprecatedRPC - systems["websocket_rpc"] = bot.Settings.EnableWebsocketRPC - systems["dispatch"] = dispatch.IsRunning() + systems[SyncManagerName] = bot.CommunicationsManager.IsRunning() + systems[ConnectionManagerName] = bot.connectionManager.IsRunning() + systems[OrderManagerName] = bot.OrderManager.IsRunning() + systems[PortfolioManagerName] = bot.portfolioManager.IsRunning() + systems[NTPManagerName] = bot.ntpManager.IsRunning() + systems[DatabaseConnectionManagerName] = bot.DatabaseManager.IsRunning() + systems[SyncManagerName] = bot.Settings.EnableExchangeSyncManager + systems[grpcName] = bot.Settings.EnableGRPC + systems[grpcProxyName] = bot.Settings.EnableGRPCProxy + systems[vm.Name] = bot.gctScriptManager.IsRunning() + systems[DeprecatedName] = bot.Settings.EnableDeprecatedRPC + systems[WebsocketName] = bot.Settings.EnableWebsocketRPC + systems[dispatch.Name] = dispatch.IsRunning() return systems } @@ -66,19 +67,19 @@ type RPCEndpoint struct { // GetRPCEndpoints returns a list of RPC endpoints and their listen addrs func GetRPCEndpoints() map[string]RPCEndpoint { endpoints := make(map[string]RPCEndpoint) - endpoints["grpc"] = RPCEndpoint{ + endpoints[grpcName] = RPCEndpoint{ Started: Bot.Settings.EnableGRPC, ListenAddr: "grpc://" + Bot.Config.RemoteControl.GRPC.ListenAddress, } - endpoints["grpc_proxy"] = RPCEndpoint{ + endpoints[grpcProxyName] = RPCEndpoint{ Started: Bot.Settings.EnableGRPCProxy, ListenAddr: "http://" + Bot.Config.RemoteControl.GRPC.GRPCProxyListenAddress, } - endpoints["deprecated_rpc"] = RPCEndpoint{ + endpoints[DeprecatedName] = RPCEndpoint{ Started: Bot.Settings.EnableDeprecatedRPC, ListenAddr: "http://" + Bot.Config.RemoteControl.DeprecatedRPC.ListenAddress, } - endpoints["websocket_rpc"] = RPCEndpoint{ + endpoints[WebsocketName] = RPCEndpoint{ Started: Bot.Settings.EnableWebsocketRPC, ListenAddr: "ws://" + Bot.Config.RemoteControl.WebsocketRPC.ListenAddress, } @@ -86,53 +87,157 @@ func GetRPCEndpoints() map[string]RPCEndpoint { } // SetSubsystem enables or disables an engine subsystem -func (bot *Engine) SetSubsystem(subsys string, enable bool) error { - switch strings.ToLower(subsys) { - case "communications": +func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error { + var err error + switch strings.ToLower(subSystemName) { + case CommunicationsManagerName: if enable { - return bot.CommsManager.Start() + if bot.CommunicationsManager == nil { + communicationsConfig := bot.Config.GetCommunicationsConfig() + bot.CommunicationsManager, err = SetupCommunicationManager(&communicationsConfig) + if err != nil { + return err + } + } + return bot.CommunicationsManager.Start() } - return bot.CommsManager.Stop() - case "internet_monitor": + return bot.CommunicationsManager.Stop() + case ConnectionManagerName: if enable { - return bot.ConnectionManager.Start(&bot.Config.ConnectionMonitor) + if bot.connectionManager == nil { + bot.connectionManager, err = setupConnectionManager(&bot.Config.ConnectionMonitor) + if err != nil { + return err + } + } + return bot.connectionManager.Start() } - return bot.CommsManager.Stop() - case "orders": + return bot.connectionManager.Stop() + case OrderManagerName: if enable { - return bot.OrderManager.Start(bot) + if bot.OrderManager == nil { + bot.OrderManager, err = SetupOrderManager( + bot.ExchangeManager, + bot.CommunicationsManager, + &bot.ServicesWG, + bot.Settings.Verbose) + if err != nil { + return err + } + } + return bot.OrderManager.Start() } return bot.OrderManager.Stop() - case "portfolio": + case PortfolioManagerName: if enable { - return bot.PortfolioManager.Start() + if bot.portfolioManager == nil { + bot.portfolioManager, err = setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, &bot.Config.Portfolio) + if err != nil { + return err + } + } + return bot.portfolioManager.Start(&bot.ServicesWG) } - return bot.OrderManager.Stop() - case "ntp_timekeeper": + return bot.portfolioManager.Stop() + case NTPManagerName: if enable { - return bot.NTPManager.Start() + if bot.ntpManager == nil { + bot.ntpManager, err = setupNTPManager( + &bot.Config.NTPClient, + *bot.Config.Logging.Enabled) + if err != nil { + return err + } + } + return bot.ntpManager.Start() } - return bot.NTPManager.Stop() - case "database": + return bot.ntpManager.Stop() + case DatabaseConnectionManagerName: if enable { - return bot.DatabaseManager.Start(bot) + if bot.DatabaseManager == nil { + bot.DatabaseManager, err = SetupDatabaseConnectionManager(&bot.Config.Database) + if err != nil { + return err + } + } + return bot.DatabaseManager.Start(&bot.ServicesWG) } return bot.DatabaseManager.Stop() - case "exchange_syncer": + case SyncManagerName: if enable { - bot.ExchangeCurrencyPairManager.Start() + if bot.currencyPairSyncer == nil { + exchangeSyncCfg := &Config{ + SyncTicker: bot.Settings.EnableTickerSyncing, + SyncOrderbook: bot.Settings.EnableOrderbookSyncing, + SyncTrades: bot.Settings.EnableTradeSyncing, + SyncContinuously: bot.Settings.SyncContinuously, + NumWorkers: bot.Settings.SyncWorkers, + Verbose: bot.Settings.Verbose, + SyncTimeoutREST: bot.Settings.SyncTimeoutREST, + SyncTimeoutWebsocket: bot.Settings.SyncTimeoutWebsocket, + } + bot.currencyPairSyncer, err = setupSyncManager(exchangeSyncCfg, + bot.ExchangeManager, + bot.websocketRoutineManager, + &bot.Config.RemoteControl) + if err != nil { + return err + } + } + return bot.currencyPairSyncer.Start() } - bot.ExchangeCurrencyPairManager.Stop() - case "dispatch": + return bot.currencyPairSyncer.Stop() + case dispatch.Name: if enable { return dispatch.Start(bot.Settings.DispatchMaxWorkerAmount, bot.Settings.DispatchJobsLimit) } return dispatch.Stop() - case "gctscript": + case DeprecatedName: if enable { - return bot.GctScriptManager.Start(&bot.ServicesWG) + 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.GctScriptManager.Stop() + 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 errors.New("cannot manage GRPC subsystem via GRPC. Please manually change your config") + + case vm.Name: + if enable { + if bot.gctScriptManager == nil { + bot.gctScriptManager, err = vm.NewManager(&bot.Config.GCTScript) + if err != nil { + return err + } + } + return bot.gctScriptManager.Start(&bot.ServicesWG) + } + return bot.gctScriptManager.Stop() } return errors.New("subsystem not found") @@ -162,9 +267,9 @@ func (bot *Engine) GetExchangeOTPs() (map[string]string, error) { return otpCodes, nil } -// GetExchangeoOTPByName returns a OTP code for the desired exchange +// GetExchangeOTPByName returns a OTP code for the desired exchange // if it exists -func (bot *Engine) GetExchangeoOTPByName(exchName string) (string, error) { +func (bot *Engine) GetExchangeOTPByName(exchName string) (string, error) { for x := range bot.Config.Exchanges { if !strings.EqualFold(bot.Config.Exchanges[x].Name, exchName) { continue @@ -193,18 +298,7 @@ func (bot *Engine) GetAuthAPISupportedExchanges() []string { // IsOnline returns whether or not the engine has Internet connectivity func (bot *Engine) IsOnline() bool { - return bot.ConnectionManager.IsOnline() -} - -// GetAvailableExchanges returns a list of enabled exchanges -func (bot *Engine) GetAvailableExchanges() []string { - var enExchanges []string - for x := range bot.Config.Exchanges { - if bot.Config.Exchanges[x].Enabled { - enExchanges = append(enExchanges, bot.Config.Exchanges[x].Name) - } - } - return enExchanges + return bot.connectionManager.IsOnline() } // GetAllAvailablePairs returns a list of all available pairs on either enabled @@ -494,90 +588,6 @@ func GetExchangeLowestPriceByCurrencyPair(p currency.Pair, assetType asset.Item) return result[0].Exchange, nil } -// SeedExchangeAccountInfo seeds account info -func SeedExchangeAccountInfo(accounts []account.Holdings) { - if len(accounts) == 0 { - return - } - - port := portfolio.GetPortfolio() - - for x := range accounts { - exchangeName := accounts[x].Exchange - var currencies []account.Balance - for y := range accounts[x].Accounts { - for z := range accounts[x].Accounts[y].Currencies { - var update bool - for i := range currencies { - if accounts[x].Accounts[y].Currencies[z].CurrencyName == currencies[i].CurrencyName { - currencies[i].Hold += accounts[x].Accounts[y].Currencies[z].Hold - currencies[i].TotalValue += accounts[x].Accounts[y].Currencies[z].TotalValue - update = true - } - } - - if update { - continue - } - - currencies = append(currencies, account.Balance{ - CurrencyName: accounts[x].Accounts[y].Currencies[z].CurrencyName, - TotalValue: accounts[x].Accounts[y].Currencies[z].TotalValue, - Hold: accounts[x].Accounts[y].Currencies[z].Hold, - }) - } - } - - for x := range currencies { - currencyName := currencies[x].CurrencyName - total := currencies[x].TotalValue - - if !port.ExchangeAddressExists(exchangeName, currencyName) { - if total <= 0 { - continue - } - - log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n", - exchangeName, - currencyName, - total, - portfolio.PortfolioAddressExchange) - - port.Addresses = append( - port.Addresses, - portfolio.Address{Address: exchangeName, - CoinType: currencyName, - Balance: total, - Description: portfolio.PortfolioAddressExchange}) - } else { - if total <= 0 { - log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n", - exchangeName, - currencyName) - port.RemoveExchangeAddress(exchangeName, currencyName) - } else { - balance, ok := port.GetAddressBalance(exchangeName, - portfolio.PortfolioAddressExchange, - currencyName) - if !ok { - continue - } - - if balance != total { - log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n", - exchangeName, - currencyName, - total) - port.UpdateExchangeAddressBalance(exchangeName, - currencyName, - total) - } - } - } - } - } -} - // GetCryptocurrenciesByExchange returns a list of cryptocurrencies the exchange supports func (bot *Engine) GetCryptocurrenciesByExchange(exchangeName string, enabledExchangesOnly, enabledPairs bool, assetType asset.Item) ([]string, error) { var cryptocurrencies []string @@ -636,7 +646,7 @@ func (bot *Engine) GetCryptocurrencyDepositAddressesByExchange(exchName string) // exchange func (bot *Engine) GetExchangeCryptocurrencyDepositAddress(exchName, accountID string, item currency.Code) (string, error) { if bot.DepositAddressManager != nil { - return bot.DepositAddressManager.GetDepositAddressByExchange(exchName, item) + return bot.DepositAddressManager.GetDepositAddressByExchangeAndCurrency(exchName, item) } exch := bot.GetExchangeByName(exchName) @@ -680,21 +690,16 @@ func (bot *Engine) GetExchangeCryptocurrencyDepositAddresses() map[string]map[st return result } -// FormatCurrency is a method that formats and returns a currency pair -// based on the user currency display preferences -func (bot *Engine) FormatCurrency(p currency.Pair) currency.Pair { - return p.Format(bot.Config.Currency.CurrencyPairFormat.Delimiter, - bot.Config.Currency.CurrencyPairFormat.Uppercase) -} - // GetExchangeNames returns a list of enabled or disabled exchanges func (bot *Engine) GetExchangeNames(enabledOnly bool) []string { - exchangeNames := bot.GetAvailableExchanges() - if enabledOnly { - return exchangeNames + exchanges := bot.ExchangeManager.GetExchanges() + var response []string + for i := range exchanges { + if !enabledOnly || (enabledOnly && exchanges[i].IsEnabled()) { + response = append(response, exchanges[i].GetName()) + } } - exchangeNames = append(exchangeNames, bot.Config.GetDisabledExchanges()...) - return exchangeNames + return response } // GetAllActiveTickers returns all enabled exchange tickers @@ -732,44 +737,6 @@ func (bot *Engine) GetAllActiveTickers() []EnabledExchangeCurrencies { return tickerData } -// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges -func (bot *Engine) GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { - var response AllEnabledExchangeAccounts - exchanges := bot.GetExchanges() - for x := range exchanges { - if exchanges[x] == nil || !exchanges[x].IsEnabled() { - continue - } - if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) { - if bot.Settings.Verbose { - log.Debugf(log.ExchangeSys, - "GetAllEnabledExchangeAccountInfo: Skipping %s due to disabled authenticated API support.\n", - exchanges[x].GetName()) - } - continue - } - assetTypes := exchanges[x].GetAssetTypes() - var exchangeHoldings account.Holdings - for y := range assetTypes { - accountHoldings, err := exchanges[x].FetchAccountInfo(assetTypes[y]) - if err != nil { - log.Errorf(log.ExchangeSys, - "Error encountered retrieving exchange account info for %s. Error %s\n", - exchanges[x].GetName(), - err) - continue - } - for z := range accountHoldings.Accounts { - accountHoldings.Accounts[z].AssetType = assetTypes[y] - } - exchangeHoldings.Exchange = exchanges[x].GetName() - exchangeHoldings.Accounts = append(exchangeHoldings.Accounts, accountHoldings.Accounts...) - } - response.Data = append(response.Data, exchangeHoldings) - } - return response -} - func verifyCert(pemData []byte) error { var pemBlock *pem.Block pemBlock, _ = pem.Decode(pemData) diff --git a/engine/helpers_test.go b/engine/helpers_test.go index 07a2dca9..6861cc43 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "errors" "math/big" "net" "os" @@ -25,6 +26,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" ) +var testExchange = "Bitstamp" + func CreateTestBot(t *testing.T) *Engine { bot, err := NewFromSettings(&Settings{ConfigFile: config.TestFile, EnableDryRun: true}, nil) if err != nil { @@ -35,19 +38,14 @@ func CreateTestBot(t *testing.T) *Engine { if err != nil { t.Fatalf("Failed to retrieve config currency pairs. %s", err) } - + bot.ExchangeManager = SetupExchangeManager() if bot.GetExchangeByName(testExchange) == nil { - err = bot.LoadExchange(testExchange, false, nil) - if err != nil { - t.Fatalf("SetupTest: Failed to load exchange: %s", err) - } - } - if bot.GetExchangeByName(fakePassExchange) == nil { - err = addPassingFakeExchange(testExchange, bot) + err := bot.LoadExchange(testExchange, false, nil) if err != nil { t.Fatalf("SetupTest: Failed to load exchange: %s", err) } } + return bot } @@ -62,7 +60,7 @@ func TestGetExchangeOTPs(t *testing.T) { if err != nil { t.Fatal(err) } - bCfg, err := bot.Config.GetExchangeConfig("Bitstamp") + bCfg, err := bot.Config.GetExchangeConfig(testExchange) if err != nil { t.Fatal(err) } @@ -93,18 +91,18 @@ func TestGetExchangeOTPs(t *testing.T) { func TestGetExchangeoOTPByName(t *testing.T) { bot := CreateTestBot(t) - _, err := bot.GetExchangeoOTPByName("Bitstamp") + _, err := bot.GetExchangeOTPByName(testExchange) if err == nil { t.Fatal("Expected err with no exchange OTP secrets set") } - bCfg, err := bot.Config.GetExchangeConfig("Bitstamp") + bCfg, err := bot.Config.GetExchangeConfig(testExchange) if err != nil { t.Fatal(err) } bCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP" - result, err := bot.GetExchangeoOTPByName("Bitstamp") + result, err := bot.GetExchangeOTPByName(testExchange) if err != nil { t.Fatal(err) } @@ -118,6 +116,25 @@ func TestGetExchangeoOTPByName(t *testing.T) { func TestGetAuthAPISupportedExchanges(t *testing.T) { e := CreateTestBot(t) + if result := e.GetAuthAPISupportedExchanges(); len(result) != 0 { + t.Fatal("Unexpected result", result) + } + + exch := e.ExchangeManager.GetExchangeByName(testExchange) + cfg, err := exch.GetDefaultConfig() + if err != nil { + t.Error(err) + } + cfg.Enabled = true + cfg.API.AuthenticatedSupport = true + cfg.API.AuthenticatedWebsocketSupport = true + cfg.API.Credentials.Key = "test" + cfg.API.Credentials.Secret = "test" + cfg.WebsocketTrafficTimeout = time.Minute + err = exch.Setup(cfg) + if err != nil { + t.Error(err) + } if result := e.GetAuthAPISupportedExchanges(); len(result) != 1 { t.Fatal("Unexpected result", result) } @@ -125,11 +142,16 @@ func TestGetAuthAPISupportedExchanges(t *testing.T) { func TestIsOnline(t *testing.T) { e := CreateTestBot(t) + var err error + e.connectionManager, err = setupConnectionManager(&e.Config.ConnectionMonitor) + if err != nil { + t.Fatal(err) + } if r := e.IsOnline(); r { t.Fatal("Unexpected result") } - if err := e.ConnectionManager.Start(&e.Config.ConnectionMonitor); err != nil { + if err = e.connectionManager.Start(); err != nil { t.Fatal(err) } @@ -141,7 +163,7 @@ func TestIsOnline(t *testing.T) { t.Fatal("Test timeout") default: if e.IsOnline() { - if err := e.ConnectionManager.Stop(); err != nil { + if err := e.connectionManager.Stop(); err != nil { t.Fatal("unable to shutdown connection manager") } return @@ -150,13 +172,6 @@ func TestIsOnline(t *testing.T) { } } -func TestGetAvailableExchanges(t *testing.T) { - e := CreateTestBot(t) - if r := len(e.GetAvailableExchanges()); r == 0 { - t.Error("Expected len > 0") - } -} - func TestGetSpecificAvailablePairs(t *testing.T) { e := CreateTestBot(t) assetType := asset.Spot @@ -475,7 +490,7 @@ func TestMapCurrenciesByExchange(t *testing.T) { } result := e.MapCurrenciesByExchange(pairs, true, asset.Spot) - pairs, ok := result["Bitstamp"] + pairs, ok := result[testExchange] if !ok { t.Fatal("Unexpected result") } @@ -507,7 +522,7 @@ func TestGetExchangeNamesByCurrency(t *testing.T) { result := e.GetExchangeNamesByCurrency(btsusd, true, assetType) - if !common.StringDataCompare(result, "Bitstamp") { + if !common.StringDataCompare(result, testExchange) { t.Fatal("Unexpected result") } @@ -529,8 +544,6 @@ func TestGetExchangeNamesByCurrency(t *testing.T) { func TestGetSpecificOrderbook(t *testing.T) { e := CreateTestBot(t) - e.LoadExchange("Bitstamp", false, nil) - var bids []orderbook.Item bids = append(bids, orderbook.Item{Price: 1000, Amount: 1}) @@ -551,7 +564,7 @@ func TestGetSpecificOrderbook(t *testing.T) { t.Fatal(err) } - ob, err := e.GetSpecificOrderbook(btsusd, "Bitstamp", asset.Spot) + ob, err := e.GetSpecificOrderbook(btsusd, testExchange, asset.Spot) if err != nil { t.Fatal(err) } @@ -565,18 +578,19 @@ func TestGetSpecificOrderbook(t *testing.T) { t.Fatal(err) } - _, err = e.GetSpecificOrderbook(ethltc, "Bitstamp", asset.Spot) + _, err = e.GetSpecificOrderbook(ethltc, testExchange, asset.Spot) if err == nil { t.Fatal("Unexpected result") } - e.UnloadExchange("Bitstamp") + err = e.UnloadExchange(testExchange) + if err != nil { + t.Error(err) + } } func TestGetSpecificTicker(t *testing.T) { e := CreateTestBot(t) - - e.LoadExchange("Bitstamp", false, nil) p, err := currency.NewPairFromStrings("BTC", "USD") if err != nil { t.Fatal(err) @@ -586,12 +600,12 @@ func TestGetSpecificTicker(t *testing.T) { Pair: p, Last: 1000, AssetType: asset.Spot, - ExchangeName: "Bitstamp"}) + ExchangeName: testExchange}) if err != nil { t.Fatal("ProcessTicker error", err) } - tick, err := e.GetSpecificTicker(p, "Bitstamp", asset.Spot) + tick, err := e.GetSpecificTicker(p, testExchange, asset.Spot) if err != nil { t.Fatal(err) } @@ -605,12 +619,15 @@ func TestGetSpecificTicker(t *testing.T) { t.Fatal(err) } - _, err = e.GetSpecificTicker(ethltc, "Bitstamp", asset.Spot) + _, err = e.GetSpecificTicker(ethltc, testExchange, asset.Spot) if err == nil { t.Fatal("Unexpected result") } - e.UnloadExchange("Bitstamp") + err = e.UnloadExchange(testExchange) + if err != nil { + t.Error(err) + } } func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) { @@ -634,7 +651,7 @@ func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) { exchangeInfo = append(exchangeInfo, bitfinexHoldings) var bitstampHoldings account.Holdings - bitstampHoldings.Exchange = "Bitstamp" + bitstampHoldings.Exchange = testExchange bitstampHoldings.Accounts = append(bitstampHoldings.Accounts, account.SubAccount{ Currencies: []account.Balance{ @@ -681,14 +698,20 @@ func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) { t.Fatal(err) } - stats.Add("Bitfinex", p, asset.Spot, 1000, 10000) - stats.Add("Bitstamp", p, asset.Spot, 1337, 10000) + err = stats.Add("Bitfinex", p, asset.Spot, 1000, 10000) + if err != nil { + t.Error(err) + } + err = stats.Add(testExchange, p, asset.Spot, 1337, 10000) + if err != nil { + t.Error(err) + } exchangeName, err := GetExchangeHighestPriceByCurrencyPair(p, asset.Spot) if err != nil { t.Error(err) } - if exchangeName != "Bitstamp" { + if exchangeName != testExchange { t.Error("Unexpected result") } @@ -711,8 +734,14 @@ func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) { t.Fatal(err) } - stats.Add("Bitfinex", p, asset.Spot, 1000, 10000) - stats.Add("Bitstamp", p, asset.Spot, 1337, 10000) + err = stats.Add("Bitfinex", p, asset.Spot, 1000, 10000) + if err != nil { + t.Error(err) + } + err = stats.Add(testExchange, p, asset.Spot, 1337, 10000) + if err != nil { + t.Error(err) + } exchangeName, err := GetExchangeLowestPriceByCurrencyPair(p, asset.Spot) if err != nil { t.Error(err) @@ -753,8 +782,22 @@ func TestGetExchangeNames(t *testing.T) { if e := bot.GetExchangeNames(true); common.StringDataCompare(e, testExchange) { t.Error("Bitstamp should be missing") } + if e := bot.GetExchangeNames(false); len(e) != 0 { + t.Errorf("Expected %v Received %v", len(e), 0) + } + + for i := range bot.Config.Exchanges { + exch, err := bot.ExchangeManager.NewExchangeByName(bot.Config.Exchanges[i].Name) + if err != nil && !errors.Is(err, ErrExchangeAlreadyLoaded) { + t.Error(err) + } + if exch != nil { + exch.SetDefaults() + bot.ExchangeManager.Add(exch) + } + } if e := bot.GetExchangeNames(false); len(e) != len(bot.Config.Exchanges) { - t.Errorf("Expected %v Received %v", len(e), len(bot.Config.Exchanges)) + t.Errorf("Expected %v Received %v", len(bot.Config.Exchanges), len(e)) } } diff --git a/engine/ntp_manager.go b/engine/ntp_manager.go new file mode 100644 index 00000000..3919ffed --- /dev/null +++ b/engine/ntp_manager.go @@ -0,0 +1,205 @@ +package engine + +import ( + "encoding/binary" + "fmt" + "net" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// setupNTPManager creates a new NTP manager +func setupNTPManager(cfg *config.NTPClientConfig, loggingEnabled bool) (*ntpManager, error) { + if cfg == nil { + return nil, errNilConfig + } + if cfg.AllowedNegativeDifference == nil || + cfg.AllowedDifference == nil { + return nil, errNilConfigValues + } + return &ntpManager{ + shutdown: make(chan struct{}), + level: int64(cfg.Level), + allowedDifference: *cfg.AllowedDifference, + allowedNegativeDifference: *cfg.AllowedNegativeDifference, + pools: cfg.Pool, + checkInterval: defaultNTPCheckInterval, + retryLimit: defaultRetryLimit, + loggingEnabled: loggingEnabled, + }, nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *ntpManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Start runs the subsystem +func (m *ntpManager) Start() error { + if m == nil { + return fmt.Errorf("ntp manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("NTP manager %w", ErrSubSystemAlreadyStarted) + } + if m.level == 0 && m.loggingEnabled { + // Sometimes the NTP client can have transient issues due to UDP, try + // the default retry limits before giving up + check: + for i := 0; i < m.retryLimit; i++ { + err := m.processTime() + switch err { + case nil: + break check + case ErrSubSystemNotStarted: + log.Debugln(log.TimeMgr, "NTP manager: User disabled NTP prompts. Exiting.") + atomic.CompareAndSwapInt32(&m.started, 1, 0) + return nil + default: + if i == m.retryLimit-1 { + return err + } + } + } + } + if m.level != 1 { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + return errNTPManagerDisabled + } + m.shutdown = make(chan struct{}) + go m.run() + log.Debugf(log.TimeMgr, "NTP manager %s", MsgSubSystemStarted) + return nil +} + +// Stop attempts to shutdown the subsystem +func (m *ntpManager) Stop() error { + if m == nil { + return fmt.Errorf("ntp manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("NTP manager %w", ErrSubSystemNotStarted) + } + defer func() { + log.Debugf(log.TimeMgr, "NTP manager %s", MsgSubSystemShutdown) + atomic.CompareAndSwapInt32(&m.started, 1, 0) + }() + log.Debugf(log.TimeMgr, "NTP manager %s", MsgSubSystemShuttingDown) + close(m.shutdown) + return nil +} + +// continuously checks the internet connection at intervals +func (m *ntpManager) run() { + t := time.NewTicker(m.checkInterval) + defer func() { + t.Stop() + }() + + for { + select { + case <-m.shutdown: + return + case <-t.C: + err := m.processTime() + if err != nil { + log.Error(log.TimeMgr, err) + } + } + } +} + +// FetchNTPTime returns the time from defined NTP pools +func (m *ntpManager) FetchNTPTime() (time.Time, error) { + if m == nil { + return time.Time{}, fmt.Errorf("ntp manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return time.Time{}, fmt.Errorf("NTP manager %w", ErrSubSystemNotStarted) + } + return checkTimeInPools(m.pools), nil +} + +// processTime determines the difference between system time and NTP time +// to discover discrepancies +func (m *ntpManager) processTime() error { + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("NTP manager %w", ErrSubSystemNotStarted) + } + NTPTime, err := m.FetchNTPTime() + if err != nil { + return err + } + currentTime := time.Now() + diff := NTPTime.Sub(currentTime) + configNTPTime := m.allowedDifference + negDiff := m.allowedNegativeDifference + configNTPNegativeTime := -negDiff + if diff > configNTPTime || diff < configNTPNegativeTime { + log.Warnf(log.TimeMgr, "NTP manager: Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v\n", + NTPTime, + currentTime, + diff, + configNTPTime, + configNTPNegativeTime) + } + return nil +} + +// checkTimeInPools returns local based on ntp servers provided timestamp +// if no server can be reached will return local time in UTC() +func checkTimeInPools(pool []string) time.Time { + for i := range pool { + con, err := net.DialTimeout("udp", pool[i], 5*time.Second) + if err != nil { + log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", pool[i]) + continue + } + + if err = con.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + log.Warnf(log.TimeMgr, "Unable to SetDeadline. Error: %s\n", err) + err = con.Close() + if err != nil { + log.Error(log.TimeMgr, err) + } + continue + } + + req := &ntpPacket{Settings: 0x1B} + if err = binary.Write(con, binary.BigEndian, req); err != nil { + log.Warnf(log.TimeMgr, "Unable to write. Error: %s\n", err) + err = con.Close() + if err != nil { + log.Error(log.TimeMgr, err) + } + continue + } + + rsp := &ntpPacket{} + if err = binary.Read(con, binary.BigEndian, rsp); err != nil { + log.Warnf(log.TimeMgr, "Unable to read. Error: %s\n", err) + err = con.Close() + if err != nil { + log.Error(log.TimeMgr, err) + } + continue + } + + secs := float64(rsp.TxTimeSec) - 2208988800 + nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32 + + err = con.Close() + if err != nil { + log.Error(log.TimeMgr, err) + } + return time.Unix(int64(secs), nanos) + } + log.Warnln(log.TimeMgr, "No valid NTP servers found, using current system time") + return time.Now().UTC() +} diff --git a/engine/ntp_manager.md b/engine/ntp_manager.md new file mode 100644 index 00000000..cb3872c3 --- /dev/null +++ b/engine/ntp_manager.md @@ -0,0 +1,56 @@ +# GoCryptoTrader package Ntp_manager + + + + +[![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/ntp_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This ntp_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Ntp_manager ++ The NTP manager subsystem is used highlight discrepancies between your system time and specified NTP server times ++ It is useful for debugging and understanding why a request to an exchange may be rejected ++ The NTP manager cannot update your system clock, so when it does alert you of issues, you must take it upon yourself to change your system time in the event your requests are being rejected for being too far out of sync ++ In order to modify the behaviour of the NTP manager subsystem, you can edit the following inside your config file under `ntpclient`: + +### ntpclient + +| Config | Description | Example | +| ------ | ----------- | ------- | +| enabled | An integer value representing whether the NTP manager is enabled. It will warn you of time sync discrepancies on startup with a value of 0 and will alert you periodically with a value of 1. A value of -1 will disable the manager | `1` | +| pool | A string array of the NTP servers to check for time discrepancies | `["0.pool.ntp.org:123","pool.ntp.org:123"]` | +| allowedDifference | A Golang time.Duration representation of the allowable time discrepancy between NTP server and your system time. Any discrepancy greater than this allowance will display an alert to your logging output | `50000000` | +| allowedNegativeDifference | A Golang time.Duration representation of the allowable negative time discrepancy between NTP server and your system time. Any discrepancy greater than this allowance will display an alert to your logging output | `50000000` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/ntp_manager_test.go b/engine/ntp_manager_test.go new file mode 100644 index 00000000..f1bc491d --- /dev/null +++ b/engine/ntp_manager_test.go @@ -0,0 +1,206 @@ +package engine + +import ( + "errors" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +func TestSetupNTPManager(t *testing.T) { + _, err := setupNTPManager(nil, false) + if !errors.Is(err, errNilConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilConfig) + } + _, err = setupNTPManager(&config.NTPClientConfig{}, false) + if !errors.Is(err, errNilConfigValues) { + t.Errorf("error '%v', expected '%v'", err, errNilConfigValues) + } + sec := time.Second + cfg := &config.NTPClientConfig{ + AllowedDifference: &sec, + AllowedNegativeDifference: &sec, + } + m, err := setupNTPManager(cfg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expected manager") + } +} + +func TestNTPManagerIsRunning(t *testing.T) { + var m *ntpManager + if m.IsRunning() { + t.Error("expected false") + } + + sec := time.Second + cfg := &config.NTPClientConfig{ + AllowedDifference: &sec, + AllowedNegativeDifference: &sec, + Level: 1, + } + m, err := setupNTPManager(cfg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.IsRunning() { + t.Error("expected false") + } + + err = m.Start() + if err != nil { + t.Error(err) + } + if !m.IsRunning() { + t.Error("expected true") + } +} + +func TestNTPManagerStart(t *testing.T) { + var m *ntpManager + err := m.Start() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + sec := time.Second + cfg := &config.NTPClientConfig{ + AllowedDifference: &sec, + AllowedNegativeDifference: &sec, + } + m, err = setupNTPManager(cfg, true) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Start() + if !errors.Is(err, errNTPManagerDisabled) { + t.Errorf("error '%v', expected '%v'", err, errNTPManagerDisabled) + } + + m.level = 1 + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } +} + +func TestNTPManagerStop(t *testing.T) { + var m *ntpManager + err := m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + sec := time.Second + cfg := &config.NTPClientConfig{ + AllowedDifference: &sec, + AllowedNegativeDifference: &sec, + Level: 1, + } + m, err = setupNTPManager(cfg, true) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestFetchNTPTime(t *testing.T) { + var m *ntpManager + _, err := m.FetchNTPTime() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + sec := time.Second + cfg := &config.NTPClientConfig{ + AllowedDifference: &sec, + AllowedNegativeDifference: &sec, + Level: 1, + } + m, err = setupNTPManager(cfg, true) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + _, err = m.FetchNTPTime() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + tt, err := m.FetchNTPTime() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if tt.IsZero() { + t.Error("expected time") + } + + m.pools = []string{"0.pool.ntp.org:123"} + tt, err = m.FetchNTPTime() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if tt.IsZero() { + t.Error("expected time") + } +} + +func TestProcessTime(t *testing.T) { + sec := time.Second + cfg := &config.NTPClientConfig{ + AllowedDifference: &sec, + AllowedNegativeDifference: &sec, + Level: 1, + Pool: []string{"0.pool.ntp.org:123"}, + } + m, err := setupNTPManager(cfg, true) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.processTime() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.processTime() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + m.allowedDifference = time.Duration(1) + m.allowedNegativeDifference = time.Duration(1) + err = m.processTime() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} diff --git a/engine/ntp_manager_types.go b/engine/ntp_manager_types.go new file mode 100644 index 00000000..d286b3d4 --- /dev/null +++ b/engine/ntp_manager_types.go @@ -0,0 +1,49 @@ +package engine + +import ( + "errors" + "time" +) + +const ( + defaultNTPCheckInterval = time.Second * 30 + defaultRetryLimit = 3 + // NTPManagerName is an exported subsystem name + NTPManagerName = "ntp_timekeeper" +) + +var ( + errNilConfigValues = errors.New("nil allowed time differences received") + errNTPManagerDisabled = errors.New("NTP manager disabled") +) + +// ntpManager starts the NTP manager +type ntpManager struct { + started int32 + shutdown chan struct{} + level int64 + allowedDifference time.Duration + allowedNegativeDifference time.Duration + pools []string + checkInterval time.Duration + retryLimit int + loggingEnabled bool +} + +type ntpPacket struct { + Settings uint8 // leap yr indicator, ver number, and mode + Stratum uint8 // stratum of local clock + Poll int8 // poll exponent + Precision int8 // precision exponent + RootDelay uint32 // root delay + RootDispersion uint32 // root dispersion + ReferenceID uint32 // reference id + RefTimeSec uint32 // reference timestamp sec + RefTimeFrac uint32 // reference timestamp fractional + OrigTimeSec uint32 // origin time secs + OrigTimeFrac uint32 // origin time fractional + RxTimeSec uint32 // receive time secs + RxTimeFrac uint32 // receive time frac + TxTimeSec uint32 // transmit time secs + TxTimeFrac uint32 // transmit time frac +} diff --git a/engine/order_manager.go b/engine/order_manager.go new file mode 100644 index 00000000..91ba35f6 --- /dev/null +++ b/engine/order_manager.go @@ -0,0 +1,738 @@ +package engine + +import ( + "errors" + "fmt" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// SetupOrderManager will boot up the OrderManager +func SetupOrderManager(exchangeManager iExchangeManager, communicationsManager iCommsManager, wg *sync.WaitGroup, verbose bool) (*OrderManager, error) { + if exchangeManager == nil { + return nil, errNilExchangeManager + } + if communicationsManager == nil { + return nil, errNilCommunicationsManager + } + if wg == nil { + return nil, errNilWaitGroup + } + + return &OrderManager{ + shutdown: make(chan struct{}), + orderStore: store{ + Orders: make(map[string][]*order.Detail), + exchangeManager: exchangeManager, + commsManager: communicationsManager, + wg: wg, + }, + verbose: verbose, + }, nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *OrderManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Start runs the subsystem +func (m *OrderManager) Start() error { + if m == nil { + return fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("order manager %w", ErrSubSystemAlreadyStarted) + } + log.Debugln(log.OrderMgr, "Order manager starting...") + m.shutdown = make(chan struct{}) + go m.run() + return nil +} + +// Stop attempts to shutdown the subsystem +func (m *OrderManager) Stop() error { + if m == nil { + return fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + + defer func() { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + }() + + log.Debugln(log.OrderMgr, "Order manager shutting down...") + close(m.shutdown) + return nil +} + +// gracefulShutdown cancels all orders (if enabled) before shutting down +func (m *OrderManager) gracefulShutdown() { + if m.cfg.CancelOrdersOnShutdown { + log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...") + m.CancelAllOrders(m.orderStore.exchangeManager.GetExchanges()) + } +} + +// run will periodically process orders +func (m *OrderManager) run() { + log.Debugln(log.OrderMgr, "Order manager started.") + tick := time.NewTicker(orderManagerDelay) + m.orderStore.wg.Add(1) + defer func() { + log.Debugln(log.OrderMgr, "Order manager shutdown.") + tick.Stop() + m.orderStore.wg.Done() + }() + + for { + select { + case <-m.shutdown: + m.gracefulShutdown() + return + case <-tick.C: + go m.processOrders() + } + } +} + +// CancelAllOrders iterates and cancels all orders for each exchange provided +func (m *OrderManager) CancelAllOrders(exchangeNames []exchange.IBotExchange) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return + } + + orders := m.orderStore.get() + if orders == nil { + return + } + + for i := range exchangeNames { + exchangeOrders, ok := orders[strings.ToLower(exchangeNames[i].GetName())] + if !ok { + continue + } + for j := range exchangeOrders { + log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.", exchangeNames[i].GetName()) + err := m.Cancel(&order.Cancel{ + Exchange: exchangeOrders[j].Exchange, + ID: exchangeOrders[j].ID, + AccountID: exchangeOrders[j].AccountID, + ClientID: exchangeOrders[j].ClientID, + WalletAddress: exchangeOrders[j].WalletAddress, + Type: exchangeOrders[j].Type, + Side: exchangeOrders[j].Side, + Pair: exchangeOrders[j].Pair, + AssetType: exchangeOrders[j].AssetType, + }) + if err != nil { + log.Error(log.OrderMgr, err) + } + } + } +} + +// Cancel will find the order in the OrderManager, send a cancel request +// to the exchange and if successful, update the status of the order +func (m *OrderManager) Cancel(cancel *order.Cancel) error { + if m == nil { + return fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + var err error + defer func() { + if err != nil { + m.orderStore.commsManager.PushEvent(base.Event{ + Type: "order", + Message: err.Error(), + }) + } + }() + + if cancel == nil { + err = errors.New("order cancel param is nil") + return err + } + if cancel.Exchange == "" { + err = errors.New("order exchange name is empty") + return err + } + if cancel.ID == "" { + err = errors.New("order id is empty") + return err + } + + exch := m.orderStore.exchangeManager.GetExchangeByName(cancel.Exchange) + if exch == nil { + err = ErrExchangeNotFound + return err + } + + if cancel.AssetType.String() != "" && !exch.GetAssetTypes().Contains(cancel.AssetType) { + err = errors.New("order asset type not supported by exchange") + return err + } + + log.Debugf(log.OrderMgr, "Order manager: Cancelling order ID %v [%+v]", + cancel.ID, cancel) + + err = exch.CancelOrder(cancel) + if err != nil { + err = fmt.Errorf("%v - Failed to cancel order: %w", cancel.Exchange, err) + return err + } + var od *order.Detail + od, err = m.orderStore.getByExchangeAndID(cancel.Exchange, cancel.ID) + if err != nil { + err = fmt.Errorf("%v - Failed to retrieve order %v to update cancelled status: %w", cancel.Exchange, cancel.ID, err) + return err + } + + od.Status = order.Cancelled + msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.", + od.Exchange, od.ID) + log.Debugln(log.OrderMgr, msg) + m.orderStore.commsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + + return nil +} + +// GetOrderInfo calls the exchange's wrapper GetOrderInfo function +// and stores the result in the order manager +func (m *OrderManager) GetOrderInfo(exchangeName, orderID string, cp currency.Pair, a asset.Item) (order.Detail, error) { + if m == nil { + return order.Detail{}, fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return order.Detail{}, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + + if orderID == "" { + return order.Detail{}, ErrOrderIDCannotBeEmpty + } + + exch := m.orderStore.exchangeManager.GetExchangeByName(exchangeName) + if exch == nil { + return order.Detail{}, ErrExchangeNotFound + } + result, err := exch.GetOrderInfo(orderID, cp, a) + if err != nil { + return order.Detail{}, err + } + + err = m.orderStore.add(&result) + if err != nil && err != ErrOrdersAlreadyExists { + return order.Detail{}, err + } + + return result, nil +} + +// validate ensures a submitted order is valid before adding to the manager +func (m *OrderManager) validate(newOrder *order.Submit) error { + if newOrder == nil { + return errors.New("order cannot be nil") + } + + if newOrder.Exchange == "" { + return errors.New("order exchange name must be specified") + } + + if err := newOrder.Validate(); err != nil { + return fmt.Errorf("order manager: %w", err) + } + + if m.cfg.EnforceLimitConfig { + if !m.cfg.AllowMarketOrders && newOrder.Type == order.Market { + return errors.New("order market type is not allowed") + } + + if m.cfg.LimitAmount > 0 && newOrder.Amount > m.cfg.LimitAmount { + return errors.New("order limit exceeds allowed limit") + } + + if len(m.cfg.AllowedExchanges) > 0 && + !common.StringDataCompareInsensitive(m.cfg.AllowedExchanges, newOrder.Exchange) { + return errors.New("order exchange not found in allowed list") + } + + if len(m.cfg.AllowedPairs) > 0 && !m.cfg.AllowedPairs.Contains(newOrder.Pair, true) { + return errors.New("order pair not found in allowed list") + } + } + return nil +} + +// Submit will take in an order struct, send it to the exchange and +// populate it in the OrderManager if successful +func (m *OrderManager) Submit(newOrder *order.Submit) (*OrderSubmitResponse, error) { + if m == nil { + return nil, fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + + err := m.validate(newOrder) + if err != nil { + return nil, err + } + exch := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange) + if exch == nil { + return nil, ErrExchangeNotFound + } + + // Checks for exchange min max limits for order amounts before order + // execution can occur + err = exch.CheckOrderExecutionLimits(newOrder.AssetType, + newOrder.Pair, + newOrder.Price, + newOrder.Amount, + newOrder.Type) + if err != nil { + return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w", + newOrder.Exchange, + err) + } + + result, err := exch.SubmitOrder(newOrder) + if err != nil { + return nil, err + } + + return m.processSubmittedOrder(newOrder, result) +} + +// SubmitFakeOrder runs through the same process as order submission +// but does not touch live endpoints +func (m *OrderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse, checkExchangeLimits bool) (*OrderSubmitResponse, error) { + if m == nil { + return nil, fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + + err := m.validate(newOrder) + if err != nil { + return nil, err + } + exch := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange) + if exch == nil { + return nil, ErrExchangeNotFound + } + + if checkExchangeLimits { + // Checks for exchange min max limits for order amounts before order + // execution can occur + err = exch.CheckOrderExecutionLimits(newOrder.AssetType, + newOrder.Pair, + newOrder.Price, + newOrder.Amount, + newOrder.Type) + if err != nil { + return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w", + newOrder.Exchange, + err) + } + } + return m.processSubmittedOrder(newOrder, resultingOrder) +} + +// GetOrdersSnapshot returns a snapshot of all orders in the orderstore. It optionally filters any orders that do not match the status +// but a status of "" or ANY will include all +// the time adds contexts for the when the snapshot is relevant for +func (m *OrderManager) GetOrdersSnapshot(s order.Status) ([]order.Detail, time.Time) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return nil, time.Time{} + } + var os []order.Detail + var latestUpdate time.Time + for _, v := range m.orderStore.Orders { + for i := range v { + if s != v[i].Status && + s != order.AnyStatus && + s != "" { + continue + } + if v[i].LastUpdated.After(latestUpdate) { + latestUpdate = v[i].LastUpdated + } + + cpy := *v[i] + os = append(os, cpy) + } + } + + return os, latestUpdate +} + +// processSubmittedOrder adds a new order to the manager +func (m *OrderManager) processSubmittedOrder(newOrder *order.Submit, result order.SubmitResponse) (*OrderSubmitResponse, error) { + if !result.IsOrderPlaced { + return nil, errors.New("order unable to be placed") + } + + id, err := uuid.NewV4() + if err != nil { + log.Warnf(log.OrderMgr, + "Order manager: Unable to generate UUID. Err: %s", + err) + } + msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v for time %v.", + newOrder.Exchange, + result.OrderID, + id.String(), + newOrder.Pair, + newOrder.Price, + newOrder.Amount, + newOrder.Side, + newOrder.Type, + newOrder.Date) + + log.Debugln(log.OrderMgr, msg) + m.orderStore.commsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + status := order.New + if result.FullyMatched { + status = order.Filled + } + err = m.orderStore.add(&order.Detail{ + ImmediateOrCancel: newOrder.ImmediateOrCancel, + HiddenOrder: newOrder.HiddenOrder, + FillOrKill: newOrder.FillOrKill, + PostOnly: newOrder.PostOnly, + Price: newOrder.Price, + Amount: newOrder.Amount, + LimitPriceUpper: newOrder.LimitPriceUpper, + LimitPriceLower: newOrder.LimitPriceLower, + TriggerPrice: newOrder.TriggerPrice, + TargetAmount: newOrder.TargetAmount, + ExecutedAmount: newOrder.ExecutedAmount, + RemainingAmount: newOrder.RemainingAmount, + Fee: newOrder.Fee, + Exchange: newOrder.Exchange, + InternalOrderID: id.String(), + ID: result.OrderID, + AccountID: newOrder.AccountID, + ClientID: newOrder.ClientID, + WalletAddress: newOrder.WalletAddress, + Type: newOrder.Type, + Side: newOrder.Side, + Status: status, + AssetType: newOrder.AssetType, + Date: time.Now(), + LastUpdated: time.Now(), + Pair: newOrder.Pair, + Leverage: newOrder.Leverage, + }) + if err != nil { + return nil, fmt.Errorf("unable to add %v order %v to orderStore: %s", newOrder.Exchange, result.OrderID, err) + } + + return &OrderSubmitResponse{ + SubmitResponse: order.SubmitResponse{ + IsOrderPlaced: result.IsOrderPlaced, + OrderID: result.OrderID, + }, + InternalOrderID: id.String(), + }, nil +} + +// processOrders iterates over all exchange orders via API +// and adds them to the internal order store +func (m *OrderManager) processOrders() { + exchanges := m.orderStore.exchangeManager.GetExchanges() + for i := range exchanges { + if !exchanges[i].GetAuthenticatedAPISupport(exchange.RestAuthentication) { + continue + } + log.Debugf(log.OrderMgr, + "Order manager: Processing orders for exchange %v.", + exchanges[i].GetName()) + + supportedAssets := exchanges[i].GetAssetTypes() + for y := range supportedAssets { + pairs, err := exchanges[i].GetEnabledPairs(supportedAssets[y]) + if err != nil { + log.Errorf(log.OrderMgr, + "Order manager: Unable to get enabled pairs for %s and asset type %s: %s", + exchanges[i].GetName(), + supportedAssets[y], + err) + continue + } + + if len(pairs) == 0 { + if m.verbose { + log.Debugf(log.OrderMgr, + "Order manager: No pairs enabled for %s and asset type %s, skipping...", + exchanges[i].GetName(), + supportedAssets[y]) + } + continue + } + + req := order.GetOrdersRequest{ + Side: order.AnySide, + Type: order.AnyType, + Pairs: pairs, + AssetType: supportedAssets[y], + } + result, err := exchanges[i].GetActiveOrders(&req) + if err != nil { + log.Warnf(log.OrderMgr, + "Order manager: Unable to get active orders for %s and asset type %s: %s", + exchanges[i].GetName(), + supportedAssets[y], + err) + continue + } + + for z := range result { + ord := &result[z] + result := m.orderStore.add(ord) + if result != ErrOrdersAlreadyExists { + msg := fmt.Sprintf("Order manager: Exchange %s added order ID=%v pair=%v price=%v amount=%v side=%v type=%v.", + ord.Exchange, ord.ID, ord.Pair, ord.Price, ord.Amount, ord.Side, ord.Type) + log.Debugf(log.OrderMgr, "%v", msg) + m.orderStore.commsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + continue + } + } + } + } +} + +// Exists checks whether an order exists in the order store +func (m *OrderManager) Exists(o *order.Detail) bool { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return false + } + + return m.orderStore.exists(o) +} + +// Add adds an order to the orderstore +func (m *OrderManager) Add(o *order.Detail) error { + if m == nil { + return fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + + return m.orderStore.add(o) +} + +// GetByExchangeAndID returns a copy of an order from an exchange if it matches the ID +func (m *OrderManager) GetByExchangeAndID(exchangeName, id string) (*order.Detail, error) { + if m == nil { + return nil, fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + + o, err := m.orderStore.getByExchangeAndID(exchangeName, id) + if err != nil { + return nil, err + } + var cpy order.Detail + cpy.UpdateOrderFromDetail(o) + return &cpy, nil +} + +// UpdateExistingOrder will update an existing order in the orderstore +func (m *OrderManager) UpdateExistingOrder(od *order.Detail) error { + if m == nil { + return fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + return m.orderStore.updateExisting(od) +} + +// UpsertOrder updates an existing order or adds a new one to the orderstore +func (m *OrderManager) UpsertOrder(od *order.Detail) error { + if m == nil { + return fmt.Errorf("order manager %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("order manager %w", ErrSubSystemNotStarted) + } + return m.orderStore.upsert(od) +} + +// get returns all orders for all exchanges +// should not be exported as it can have large impact if used improperly +func (s *store) get() map[string][]*order.Detail { + s.m.Lock() + orders := s.Orders + s.m.Unlock() + return orders +} + +// getByExchangeAndID returns a specific order by exchange and id +func (s *store) getByExchangeAndID(exchange, id string) (*order.Detail, error) { + s.m.Lock() + defer s.m.Unlock() + r, ok := s.Orders[strings.ToLower(exchange)] + if !ok { + return nil, ErrExchangeNotFound + } + + for x := range r { + if r[x].ID == id { + return r[x], nil + } + } + return nil, ErrOrderNotFound +} + +// updateExisting checks if an order exists in the orderstore +// and then updates it +func (s *store) updateExisting(od *order.Detail) error { + s.m.Lock() + defer s.m.Unlock() + r, ok := s.Orders[strings.ToLower(od.Exchange)] + if !ok { + return ErrExchangeNotFound + } + for x := range r { + if r[x].ID == od.ID { + r[x].UpdateOrderFromDetail(od) + return nil + } + } + + return ErrOrderNotFound +} + +func (s *store) upsert(od *order.Detail) error { + lName := strings.ToLower(od.Exchange) + exch := s.exchangeManager.GetExchangeByName(lName) + if exch == nil { + return ErrExchangeNotFound + } + s.m.Lock() + defer s.m.Unlock() + r, ok := s.Orders[lName] + if !ok { + s.Orders[lName] = []*order.Detail{od} + return nil + } + for x := range r { + if r[x].ID == od.ID { + r[x].UpdateOrderFromDetail(od) + return nil + } + } + s.Orders[lName] = append(s.Orders[lName], od) + return nil +} + +// getByExchange returns orders by exchange +func (s *store) getByExchange(exchange string) ([]*order.Detail, error) { + s.m.Lock() + defer s.m.Unlock() + r, ok := s.Orders[strings.ToLower(exchange)] + if !ok { + return nil, ErrExchangeNotFound + } + return r, nil +} + +// getByInternalOrderID will search all orders for our internal orderID +// and return the order +func (s *store) getByInternalOrderID(internalOrderID string) (*order.Detail, error) { + s.m.Lock() + defer s.m.Unlock() + for _, v := range s.Orders { + for x := range v { + if v[x].InternalOrderID == internalOrderID { + return v[x], nil + } + } + } + return nil, ErrOrderNotFound +} + +// exists verifies if the orderstore contains the provided order +func (s *store) exists(det *order.Detail) bool { + if det == nil { + return false + } + s.m.Lock() + defer s.m.Unlock() + r, ok := s.Orders[strings.ToLower(det.Exchange)] + if !ok { + return false + } + + for x := range r { + if r[x].ID == det.ID { + return true + } + } + return false +} + +// Add Adds an order to the orderStore for tracking the lifecycle +func (s *store) add(det *order.Detail) error { + if det == nil { + return errors.New("order store: Order is nil") + } + exch := s.exchangeManager.GetExchangeByName(det.Exchange) + if exch == nil { + return ErrExchangeNotFound + } + if s.exists(det) { + return ErrOrdersAlreadyExists + } + // Untracked websocket orders will not have internalIDs yet + if det.InternalOrderID == "" { + id, err := uuid.NewV4() + if err != nil { + log.Warnf(log.OrderMgr, + "Order manager: Unable to generate UUID. Err: %s", + err) + } else { + det.InternalOrderID = id.String() + } + } + s.m.Lock() + defer s.m.Unlock() + orders := s.Orders[strings.ToLower(det.Exchange)] + orders = append(orders, det) + s.Orders[strings.ToLower(det.Exchange)] = orders + + return nil +} diff --git a/engine/order_manager.md b/engine/order_manager.md new file mode 100644 index 00000000..7b14a06e --- /dev/null +++ b/engine/order_manager.md @@ -0,0 +1,45 @@ +# GoCryptoTrader package Order_manager + + + + +[![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/order_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This order_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Order_manager ++ The order manager subsystem stores and monitors all orders from enabled exchanges with API keys and `authenticatedSupport` enabled ++ It can be enabled or disabled via runtime command `-ordermanager=false` and defaults to true ++ All orders placed via GoCryptoTrader will be added to the order manager store + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/order_manager_test.go b/engine/order_manager_test.go new file mode 100644 index 00000000..2eaf7145 --- /dev/null +++ b/engine/order_manager_test.go @@ -0,0 +1,561 @@ +package engine + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +// omfExchange aka ordermanager fake exchange overrides exchange functions +// we're not testing an actual exchange's implemented functions +type omfExchange struct { + exchange.IBotExchange +} + +// CancelOrder overrides testExchange's cancel order function +// to do the bare minimum required with no API calls or credentials required +func (f omfExchange) CancelOrder(o *order.Cancel) error { + o.Status = order.Cancelled + return nil +} + +// GetOrderInfo overrides testExchange's get order function +// to do the bare minimum required with no API calls or credentials required +func (f omfExchange) GetOrderInfo(orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) { + if orderID == "" { + return order.Detail{}, errors.New("") + } + + return order.Detail{ + Exchange: testExchange, + ID: orderID, + Pair: pair, + AssetType: assetType, + }, nil +} + +func TestSetupOrderManager(t *testing.T) { + _, err := SetupOrderManager(nil, nil, nil, false) + if !errors.Is(err, errNilExchangeManager) { + t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) + } + + _, err = SetupOrderManager(SetupExchangeManager(), nil, nil, false) + if !errors.Is(err, errNilCommunicationsManager) { + t.Errorf("error '%v', expected '%v'", err, errNilCommunicationsManager) + } + _, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, nil, false) + if !errors.Is(err, errNilWaitGroup) { + t.Errorf("error '%v', expected '%v'", err, errNilWaitGroup) + } + var wg sync.WaitGroup + _, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestOrderManagerStart(t *testing.T) { + var m *OrderManager + err := m.Start() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + var wg sync.WaitGroup + m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } +} + +func TestOrderManagerIsRunning(t *testing.T) { + var m *OrderManager + if m.IsRunning() { + t.Error("expected false") + } + + var wg sync.WaitGroup + m, err := SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.IsRunning() { + t.Error("expected false") + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if !m.IsRunning() { + t.Error("expected true") + } +} + +func TestOrderManagerStop(t *testing.T) { + var m *OrderManager + err := m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + var wg sync.WaitGroup + m, err = SetupOrderManager(SetupExchangeManager(), &CommunicationManager{}, &wg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func OrdersSetup(t *testing.T) *OrderManager { + var wg sync.WaitGroup + em := SetupExchangeManager() + exch, err := em.NewExchangeByName(testExchange) + if err != nil { + t.Error(err) + } + exch.SetDefaults() + + fakeExchange := omfExchange{ + IBotExchange: exch, + } + em.Add(fakeExchange) + m, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + return m +} + +func TestOrdersGet(t *testing.T) { + m := OrdersSetup(t) + if m.orderStore.get() == nil { + t.Error("orderStore not established") + } +} + +func TestOrdersAdd(t *testing.T) { + m := OrdersSetup(t) + err := m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestOrdersAdd", + }) + if err != nil { + t.Error(err) + } + err = m.orderStore.add(&order.Detail{ + Exchange: "testTest", + ID: "TestOrdersAdd", + }) + if err == nil { + t.Error("Expected error from non existent exchange") + } + + err = m.orderStore.add(nil) + if err == nil { + t.Error("Expected error from nil order") + } + + err = m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestOrdersAdd", + }) + if err == nil { + t.Error("Expected error re-adding order") + } +} + +func TestGetByInternalOrderID(t *testing.T) { + m := OrdersSetup(t) + err := m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByInternalOrderID", + InternalOrderID: "internalTest", + }) + if err != nil { + t.Error(err) + } + + o, err := m.orderStore.getByInternalOrderID("internalTest") + if err != nil { + t.Error(err) + } + if o == nil { + t.Fatal("Expected a matching order") + } + if o.ID != "TestGetByInternalOrderID" { + t.Error("Expected to retrieve order") + } + + _, err = m.orderStore.getByInternalOrderID("NoOrder") + if err != ErrOrderNotFound { + t.Error(err) + } +} + +func TestGetByExchange(t *testing.T) { + m := OrdersSetup(t) + err := m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchange", + InternalOrderID: "internalTestGetByExchange", + }) + if err != nil { + t.Error(err) + } + + err = m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchange2", + InternalOrderID: "internalTestGetByExchange2", + }) + if err != nil { + t.Error(err) + } + + err = m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchange3", + InternalOrderID: "internalTest3", + }) + if err != nil { + t.Error(err) + } + var o []*order.Detail + o, err = m.orderStore.getByExchange(testExchange) + if err != nil { + t.Error(err) + } + if o == nil { + t.Error("Expected non nil response") + } + var o1Found, o2Found bool + for i := range o { + if o[i].ID == "TestGetByExchange" && o[i].Exchange == testExchange { + o1Found = true + } + if o[i].ID == "TestGetByExchange2" && o[i].Exchange == testExchange { + o2Found = true + } + } + if !o1Found || !o2Found { + t.Error("Expected orders 'TestGetByExchange' and 'TestGetByExchange2' to be returned") + } + + _, err = m.orderStore.getByInternalOrderID("NoOrder") + if err != ErrOrderNotFound { + t.Error(err) + } + err = m.orderStore.add(&order.Detail{ + Exchange: "thisWillFail", + }) + if err == nil { + t.Error("Expected exchange not found error") + } +} + +func TestGetByExchangeAndID(t *testing.T) { + m := OrdersSetup(t) + err := m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchangeAndID", + }) + if err != nil { + t.Error(err) + } + + o, err := m.orderStore.getByExchangeAndID(testExchange, "TestGetByExchangeAndID") + if err != nil { + t.Error(err) + } + if o.ID != "TestGetByExchangeAndID" { + t.Error("Expected to retrieve order") + } + + _, err = m.orderStore.getByExchangeAndID("", "TestGetByExchangeAndID") + if err != ErrExchangeNotFound { + t.Error(err) + } + + _, err = m.orderStore.getByExchangeAndID(testExchange, "") + if err != ErrOrderNotFound { + t.Error(err) + } +} + +func TestExists(t *testing.T) { + m := OrdersSetup(t) + if m.orderStore.exists(nil) { + t.Error("Expected false") + } + o := &order.Detail{ + Exchange: testExchange, + ID: "TestExists", + } + err := m.orderStore.add(o) + if err != nil { + t.Error(err) + } + b := m.orderStore.exists(o) + if !b { + t.Error("Expected true") + } +} + +func TestCancelOrder(t *testing.T) { + m := OrdersSetup(t) + + err := m.Cancel(nil) + if err == nil { + t.Error("Expected error due to empty order") + } + + err = m.Cancel(&order.Cancel{}) + if err == nil { + t.Error("Expected error due to empty order") + } + + err = m.Cancel(&order.Cancel{ + Exchange: testExchange, + }) + if err == nil { + t.Error("Expected error due to no order ID") + } + + err = m.Cancel(&order.Cancel{ + ID: "ID", + }) + if err == nil { + t.Error("Expected error due to no Exchange") + } + + err = m.Cancel(&order.Cancel{ + ID: "ID", + Exchange: testExchange, + AssetType: asset.Binary, + }) + if err == nil { + t.Error("Expected error due to bad asset type") + } + + o := &order.Detail{ + Exchange: testExchange, + ID: "1337", + Status: order.New, + } + err = m.orderStore.add(o) + if err != nil { + t.Error(err) + } + + err = m.Cancel(&order.Cancel{ + ID: "Unknown", + Exchange: testExchange, + AssetType: asset.Spot, + }) + if err == nil { + t.Error("Expected error due to no order found") + } + + pair, err := currency.NewPairFromString("BTCUSD") + if err != nil { + t.Fatal(err) + } + + cancel := &order.Cancel{ + Exchange: testExchange, + ID: "1337", + Side: order.Sell, + Status: order.New, + AssetType: asset.Spot, + Date: time.Now(), + Pair: pair, + } + err = m.Cancel(cancel) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + if o.Status != order.Cancelled { + t.Error("Failed to cancel") + } +} + +func TestGetOrderInfo(t *testing.T) { + m := OrdersSetup(t) + _, err := m.GetOrderInfo("", "", currency.Pair{}, "") + if err == nil { + t.Error("Expected error due to empty order") + } + + var result order.Detail + result, err = m.GetOrderInfo(testExchange, "1337", currency.Pair{}, "") + if err != nil { + t.Error(err) + } + if result.ID != "1337" { + t.Error("unexpected order returned") + } + + result, err = m.GetOrderInfo(testExchange, "1337", currency.Pair{}, "") + if err != nil { + t.Error(err) + } + if result.ID != "1337" { + t.Error("unexpected order returned") + } +} + +func TestCancelAllOrders(t *testing.T) { + m := OrdersSetup(t) + o := &order.Detail{ + Exchange: testExchange, + ID: "TestCancelAllOrders", + Status: order.New, + } + err := m.orderStore.add(o) + if err != nil { + t.Error(err) + } + exch := m.orderStore.exchangeManager.GetExchangeByName(testExchange) + m.CancelAllOrders([]exchange.IBotExchange{}) + if o.Status == order.Cancelled { + t.Error("Order should not be cancelled") + } + + m.CancelAllOrders([]exchange.IBotExchange{exch}) + if o.Status != order.Cancelled { + t.Error("Order should be cancelled") + } + + o.Status = order.New + m.CancelAllOrders(nil) + if o.Status != order.New { + t.Error("Order should not be cancelled") + } +} + +func TestSubmit(t *testing.T) { + m := OrdersSetup(t) + _, err := m.Submit(nil) + if err == nil { + t.Error("Expected error from nil order") + } + + o := &order.Submit{ + Exchange: "", + ID: "FakePassingExchangeOrder", + Status: order.New, + Type: order.Market, + } + _, err = m.Submit(o) + if err == nil { + t.Error("Expected error from empty exchange") + } + + o.Exchange = testExchange + _, err = m.Submit(o) + if err == nil { + t.Error("Expected error from validation") + } + + pair, err := currency.NewPairFromString("BTCUSD") + if err != nil { + t.Fatal(err) + } + + m.cfg.EnforceLimitConfig = true + m.cfg.AllowMarketOrders = false + o.Pair = pair + o.AssetType = asset.Spot + o.Side = order.Buy + o.Amount = 1 + o.Price = 1 + _, err = m.Submit(o) + if err == nil { + t.Error("Expected fail due to order market type is not allowed") + } + m.cfg.AllowMarketOrders = true + m.cfg.LimitAmount = 1 + o.Amount = 2 + _, err = m.Submit(o) + if err == nil { + t.Error("Expected fail due to order limit exceeds allowed limit") + } + m.cfg.LimitAmount = 0 + m.cfg.AllowedExchanges = []string{"fake"} + _, err = m.Submit(o) + if err == nil { + t.Error("Expected fail due to order exchange not found in allowed list") + } + + failPair, err := currency.NewPairFromString("BTCAUD") + if err != nil { + t.Fatal(err) + } + + m.cfg.AllowedExchanges = nil + m.cfg.AllowedPairs = currency.Pairs{failPair} + _, err = m.Submit(o) + if err == nil { + t.Error("Expected fail due to order pair not found in allowed list") + } + + m.cfg.AllowedPairs = nil + _, err = m.Submit(o) + if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) { + t.Errorf("error '%v', expected '%v'", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) + } + + err = m.orderStore.add(&order.Detail{ + Exchange: testExchange, + ID: "FakePassingExchangeOrder", + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + o2, err := m.orderStore.getByExchangeAndID(testExchange, "FakePassingExchangeOrder") + if err != nil { + t.Error(err) + } + if o2.InternalOrderID == "" { + t.Error("Failed to assign internal order id") + } +} + +func TestProcessOrders(t *testing.T) { + m := OrdersSetup(t) + m.processOrders() +} diff --git a/engine/order_manager_types.go b/engine/order_manager_types.go new file mode 100644 index 00000000..d77edc20 --- /dev/null +++ b/engine/order_manager_types.go @@ -0,0 +1,59 @@ +package engine + +import ( + "errors" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +// OrderManagerName is an exported subsystem name +const OrderManagerName = "orders" + +// vars for the fund manager package +var ( + orderManagerDelay = time.Second * 10 + // ErrOrdersAlreadyExists occurs when the order already exists in the manager + ErrOrdersAlreadyExists = errors.New("order already exists") + // ErrOrderNotFound occurs when an order is not found in the orderstore + ErrOrderNotFound = errors.New("order does not exist") + errNilCommunicationsManager = errors.New("cannot start with nil communications manager") + // ErrOrderIDCannotBeEmpty occurs when an order does not have an ID + ErrOrderIDCannotBeEmpty = errors.New("orderID cannot be empty") +) + +type orderManagerConfig struct { + EnforceLimitConfig bool + AllowMarketOrders bool + CancelOrdersOnShutdown bool + LimitAmount float64 + AllowedPairs currency.Pairs + AllowedExchanges []string + OrderSubmissionRetries int64 +} + +// store holds all orders by exchange +type store struct { + m sync.Mutex + Orders map[string][]*order.Detail + commsManager iCommsManager + exchangeManager iExchangeManager + wg *sync.WaitGroup +} + +// OrderManager processes and stores orders across enabled exchanges +type OrderManager struct { + started int32 + shutdown chan struct{} + orderStore store + cfg orderManagerConfig + verbose bool +} + +// OrderSubmitResponse contains the order response along with an internal order ID +type OrderSubmitResponse struct { + order.SubmitResponse + InternalOrderID string +} diff --git a/engine/orders.go b/engine/orders.go deleted file mode 100644 index bf53920c..00000000 --- a/engine/orders.go +++ /dev/null @@ -1,573 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "strings" - "sync/atomic" - "time" - - "github.com/gofrs/uuid" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// vars for the fund manager package -var ( - orderManagerDelay = time.Second * 10 - ErrOrdersAlreadyExists = errors.New("order already exists") - ErrOrderNotFound = errors.New("order does not exist") -) - -// get returns all orders for all exchanges -// should not be exported as it can have large impact if used improperly -func (o *orderStore) get() map[string][]*order.Detail { - o.m.RLock() - orders := o.Orders - o.m.RUnlock() - return orders -} - -// GetByExchangeAndID returns a specific order by exchange and id -func (o *orderStore) GetByExchangeAndID(exchange, id string) (*order.Detail, error) { - o.m.RLock() - defer o.m.RUnlock() - r, ok := o.Orders[strings.ToLower(exchange)] - if !ok { - return nil, ErrExchangeNotFound - } - - for x := range r { - if r[x].ID == id { - return r[x], nil - } - } - return nil, ErrOrderNotFound -} - -// GetByExchange returns orders by exchange -func (o *orderStore) GetByExchange(exchange string) ([]*order.Detail, error) { - o.m.RLock() - defer o.m.RUnlock() - r, ok := o.Orders[strings.ToLower(exchange)] - if !ok { - return nil, ErrExchangeNotFound - } - return r, nil -} - -// GetByInternalOrderID will search all orders for our internal orderID -// and return the order -func (o *orderStore) GetByInternalOrderID(internalOrderID string) (*order.Detail, error) { - o.m.RLock() - defer o.m.RUnlock() - for _, v := range o.Orders { - for x := range v { - if v[x].InternalOrderID == internalOrderID { - return v[x], nil - } - } - } - return nil, ErrOrderNotFound -} - -func (o *orderStore) exists(det *order.Detail) bool { - if det == nil { - return false - } - o.m.RLock() - defer o.m.RUnlock() - r, ok := o.Orders[strings.ToLower(det.Exchange)] - if !ok { - return false - } - - for x := range r { - if r[x].ID == det.ID { - return true - } - } - return false -} - -// Add Adds an order to the orderStore for tracking the lifecycle -func (o *orderStore) Add(det *order.Detail) error { - if det == nil { - return errors.New("order store: Order is nil") - } - exch := o.bot.GetExchangeByName(det.Exchange) - if exch == nil { - return ErrExchangeNotFound - } - if o.exists(det) { - return ErrOrdersAlreadyExists - } - // Untracked websocket orders will not have internalIDs yet - if det.InternalOrderID == "" { - id, err := uuid.NewV4() - if err != nil { - log.Warnf(log.OrderMgr, - "Order manager: Unable to generate UUID. Err: %s", - err) - } else { - det.InternalOrderID = id.String() - } - } - o.m.Lock() - defer o.m.Unlock() - orders := o.Orders[strings.ToLower(det.Exchange)] - orders = append(orders, det) - o.Orders[strings.ToLower(det.Exchange)] = orders - - return nil -} - -// Started returns the status of the orderManager -func (o *orderManager) Started() bool { - return atomic.LoadInt32(&o.started) == 1 -} - -// Start will boot up the orderManager -func (o *orderManager) Start(bot *Engine) error { - if bot == nil { - return errors.New("cannot start with nil bot") - } - if !atomic.CompareAndSwapInt32(&o.started, 0, 1) { - return fmt.Errorf("order manager %w", subsystem.ErrSubSystemAlreadyStarted) - } - log.Debugln(log.OrderBook, "Order manager starting...") - - o.shutdown = make(chan struct{}) - o.orderStore.Orders = make(map[string][]*order.Detail) - o.orderStore.bot = bot - - go o.run() - return nil -} - -// Stop will attempt to shutdown the orderManager -func (o *orderManager) Stop() error { - if atomic.LoadInt32(&o.started) == 0 { - return fmt.Errorf("order manager %w", subsystem.ErrSubSystemNotStarted) - } - - defer func() { - atomic.CompareAndSwapInt32(&o.started, 1, 0) - }() - - log.Debugln(log.OrderBook, "Order manager shutting down...") - close(o.shutdown) - return nil -} - -func (o *orderManager) gracefulShutdown() { - if o.cfg.CancelOrdersOnShutdown { - log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...") - o.CancelAllOrders(o.orderStore.bot.Config.GetEnabledExchanges()) - } -} - -func (o *orderManager) run() { - log.Debugln(log.OrderBook, "Order manager started.") - tick := time.NewTicker(orderManagerDelay) - o.orderStore.bot.ServicesWG.Add(1) - defer func() { - log.Debugln(log.OrderMgr, "Order manager shutdown.") - tick.Stop() - o.orderStore.bot.ServicesWG.Done() - }() - - for { - select { - case <-o.shutdown: - o.gracefulShutdown() - return - case <-tick.C: - go o.processOrders() - } - } -} - -// CancelAllOrders iterates and cancels all orders for each exchange provided -func (o *orderManager) CancelAllOrders(exchangeNames []string) { - orders := o.orderStore.get() - if orders == nil { - return - } - - for k, v := range orders { - log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.", k) - if !common.StringDataCompareInsensitive(exchangeNames, k) { - continue - } - - for y := range v { - err := o.Cancel(&order.Cancel{ - Exchange: k, - ID: v[y].ID, - AccountID: v[y].AccountID, - ClientID: v[y].ClientID, - WalletAddress: v[y].WalletAddress, - Type: v[y].Type, - Side: v[y].Side, - Pair: v[y].Pair, - AssetType: v[y].AssetType, - }) - if err != nil { - log.Error(log.OrderMgr, err) - continue - } - } - } -} - -// Cancel will find the order in the orderManager, send a cancel request -// to the exchange and if successful, update the status of the order -func (o *orderManager) Cancel(cancel *order.Cancel) error { - var err error - defer func() { - if err != nil { - o.orderStore.bot.CommsManager.PushEvent(base.Event{ - Type: "order", - Message: err.Error(), - }) - } - }() - - if cancel == nil { - err = errors.New("order cancel param is nil") - return err - } - if cancel.Exchange == "" { - err = errors.New("order exchange name is empty") - return err - } - if cancel.ID == "" { - err = errors.New("order id is empty") - return err - } - - exch := o.orderStore.bot.GetExchangeByName(cancel.Exchange) - if exch == nil { - err = ErrExchangeNotFound - return err - } - - if cancel.AssetType.String() != "" && !exch.GetAssetTypes().Contains(cancel.AssetType) { - err = errors.New("order asset type not supported by exchange") - return err - } - - log.Debugf(log.OrderMgr, "Order manager: Cancelling order ID %v [%+v]", - cancel.ID, cancel) - - err = exch.CancelOrder(cancel) - if err != nil { - err = fmt.Errorf("%v - Failed to cancel order: %v", cancel.Exchange, err) - return err - } - var od *order.Detail - od, err = o.orderStore.GetByExchangeAndID(cancel.Exchange, cancel.ID) - if err != nil { - err = fmt.Errorf("%v - Failed to retrieve order %v to update cancelled status: %v", cancel.Exchange, cancel.ID, err) - return err - } - - od.Status = order.Cancelled - msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.", - od.Exchange, od.ID) - log.Debugln(log.OrderMgr, msg) - o.orderStore.bot.CommsManager.PushEvent(base.Event{ - Type: "order", - Message: msg, - }) - - return nil -} - -// GetOrderInfo calls the exchange's wrapper GetOrderInfo function -// and stores the result in the order manager -func (o *orderManager) GetOrderInfo(exchangeName, orderID string, cp currency.Pair, a asset.Item) (order.Detail, error) { - if orderID == "" { - return order.Detail{}, errOrderIDCannotBeEmpty - } - - exch := o.orderStore.bot.GetExchangeByName(exchangeName) - if exch == nil { - return order.Detail{}, ErrExchangeNotFound - } - result, err := exch.GetOrderInfo(orderID, cp, a) - if err != nil { - return order.Detail{}, err - } - - err = o.orderStore.Add(&result) - if err != nil && err != ErrOrdersAlreadyExists { - return order.Detail{}, err - } - - return result, nil -} - -func (o *orderManager) validate(newOrder *order.Submit) error { - if newOrder == nil { - return errors.New("order cannot be nil") - } - - if newOrder.Exchange == "" { - return errors.New("order exchange name must be specified") - } - - if err := newOrder.Validate(); err != nil { - return fmt.Errorf("order manager: %w", err) - } - - if o.cfg.EnforceLimitConfig { - if !o.cfg.AllowMarketOrders && newOrder.Type == order.Market { - return errors.New("order market type is not allowed") - } - - if o.cfg.LimitAmount > 0 && newOrder.Amount > o.cfg.LimitAmount { - return errors.New("order limit exceeds allowed limit") - } - - if len(o.cfg.AllowedExchanges) > 0 && - !common.StringDataCompareInsensitive(o.cfg.AllowedExchanges, newOrder.Exchange) { - return errors.New("order exchange not found in allowed list") - } - - if len(o.cfg.AllowedPairs) > 0 && !o.cfg.AllowedPairs.Contains(newOrder.Pair, true) { - return errors.New("order pair not found in allowed list") - } - } - return nil -} - -// Submit will take in an order struct, send it to the exchange and -// populate it in the orderManager if successful -func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, error) { - err := o.validate(newOrder) - if err != nil { - return nil, err - } - exch := o.orderStore.bot.GetExchangeByName(newOrder.Exchange) - if exch == nil { - return nil, ErrExchangeNotFound - } - - // Checks for exchange min max limits for order amounts before order - // execution can occur - err = exch.CheckOrderExecutionLimits(newOrder.AssetType, - newOrder.Pair, - newOrder.Price, - newOrder.Amount, - newOrder.Type) - if err != nil { - return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w", - newOrder.Exchange, - err) - } - - result, err := exch.SubmitOrder(newOrder) - if err != nil { - return nil, err - } - - return o.processSubmittedOrder(newOrder, result) -} - -// SubmitFakeOrder runs through the same process as order submission -// but does not touch live endpoints -func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse, checkExchangeLimits bool) (*orderSubmitResponse, error) { - err := o.validate(newOrder) - if err != nil { - return nil, err - } - exch := o.orderStore.bot.GetExchangeByName(newOrder.Exchange) - if exch == nil { - return nil, ErrExchangeNotFound - } - - if checkExchangeLimits { - // Checks for exchange min max limits for order amounts before order - // execution can occur - err = exch.CheckOrderExecutionLimits(newOrder.AssetType, - newOrder.Pair, - newOrder.Price, - newOrder.Amount, - newOrder.Type) - if err != nil { - return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w", - newOrder.Exchange, - err) - } - } - return o.processSubmittedOrder(newOrder, resultingOrder) -} - -// GetOrdersSnapshot returns a snapshot of all orders in the orderstore. It optionally filters any orders that do not match the status -// but a status of "" or ANY will include all -// the time adds contexts for the when the snapshot is relevant for -func (o *orderManager) GetOrdersSnapshot(s order.Status) ([]order.Detail, time.Time) { - var os []order.Detail - var latestUpdate time.Time - for _, v := range o.orderStore.Orders { - for i := range v { - if s != v[i].Status && - s != order.AnyStatus && - s != "" { - continue - } - if v[i].LastUpdated.After(latestUpdate) { - latestUpdate = v[i].LastUpdated - } - - cpy := *v[i] - os = append(os, cpy) - } - } - - return os, latestUpdate -} - -func (o *orderManager) processSubmittedOrder(newOrder *order.Submit, result order.SubmitResponse) (*orderSubmitResponse, error) { - if !result.IsOrderPlaced { - return nil, errors.New("order unable to be placed") - } - - id, err := uuid.NewV4() - if err != nil { - log.Warnf(log.OrderMgr, - "Order manager: Unable to generate UUID. Err: %s", - err) - } - msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v for time %v.", - newOrder.Exchange, - result.OrderID, - id.String(), - newOrder.Pair, - newOrder.Price, - newOrder.Amount, - newOrder.Side, - newOrder.Type, - newOrder.Date) - - log.Debugln(log.OrderMgr, msg) - o.orderStore.bot.CommsManager.PushEvent(base.Event{ - Type: "order", - Message: msg, - }) - status := order.New - if result.FullyMatched { - status = order.Filled - } - err = o.orderStore.Add(&order.Detail{ - ImmediateOrCancel: newOrder.ImmediateOrCancel, - HiddenOrder: newOrder.HiddenOrder, - FillOrKill: newOrder.FillOrKill, - PostOnly: newOrder.PostOnly, - Price: newOrder.Price, - Amount: newOrder.Amount, - LimitPriceUpper: newOrder.LimitPriceUpper, - LimitPriceLower: newOrder.LimitPriceLower, - TriggerPrice: newOrder.TriggerPrice, - TargetAmount: newOrder.TargetAmount, - ExecutedAmount: newOrder.ExecutedAmount, - RemainingAmount: newOrder.RemainingAmount, - Fee: newOrder.Fee, - Exchange: newOrder.Exchange, - InternalOrderID: id.String(), - ID: result.OrderID, - AccountID: newOrder.AccountID, - ClientID: newOrder.ClientID, - WalletAddress: newOrder.WalletAddress, - Type: newOrder.Type, - Side: newOrder.Side, - Status: status, - AssetType: newOrder.AssetType, - Date: time.Now(), - LastUpdated: time.Now(), - Pair: newOrder.Pair, - Leverage: newOrder.Leverage, - }) - if err != nil { - return nil, fmt.Errorf("unable to add %v order %v to orderStore: %s", newOrder.Exchange, result.OrderID, err) - } - - return &orderSubmitResponse{ - SubmitResponse: order.SubmitResponse{ - IsOrderPlaced: result.IsOrderPlaced, - OrderID: result.OrderID, - }, - InternalOrderID: id.String(), - }, nil -} - -func (o *orderManager) processOrders() { - authExchanges := o.orderStore.bot.GetAuthAPISupportedExchanges() - for x := range authExchanges { - log.Debugf(log.OrderMgr, - "Order manager: Processing orders for exchange %v.", - authExchanges[x]) - - exch := o.orderStore.bot.GetExchangeByName(authExchanges[x]) - supportedAssets := exch.GetAssetTypes() - for y := range supportedAssets { - pairs, err := exch.GetEnabledPairs(supportedAssets[y]) - if err != nil { - log.Errorf(log.OrderMgr, - "Order manager: Unable to get enabled pairs for %s and asset type %s: %s", - authExchanges[x], - supportedAssets[y], - err) - continue - } - - if len(pairs) == 0 { - if o.orderStore.bot.Settings.Verbose { - log.Debugf(log.OrderMgr, - "Order manager: No pairs enabled for %s and asset type %s, skipping...", - authExchanges[x], - supportedAssets[y]) - } - continue - } - - req := order.GetOrdersRequest{ - Side: order.AnySide, - Type: order.AnyType, - Pairs: pairs, - AssetType: supportedAssets[y], - } - result, err := exch.GetActiveOrders(&req) - if err != nil { - log.Warnf(log.OrderMgr, - "Order manager: Unable to get active orders for %s and asset type %s: %s", - authExchanges[x], - supportedAssets[y], - err) - continue - } - - for z := range result { - ord := &result[z] - result := o.orderStore.Add(ord) - if result != ErrOrdersAlreadyExists { - msg := fmt.Sprintf("Order manager: Exchange %s added order ID=%v pair=%v price=%v amount=%v side=%v type=%v.", - ord.Exchange, ord.ID, ord.Pair, ord.Price, ord.Amount, ord.Side, ord.Type) - log.Debugf(log.OrderMgr, "%v", msg) - o.orderStore.bot.CommsManager.PushEvent(base.Event{ - Type: "order", - Message: msg, - }) - continue - } - } - } - } -} diff --git a/engine/orders_test.go b/engine/orders_test.go deleted file mode 100644 index 7cf87161..00000000 --- a/engine/orders_test.go +++ /dev/null @@ -1,416 +0,0 @@ -package engine - -import ( - "testing" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" -) - -func OrdersSetup(t *testing.T) *Engine { - bot := CreateTestBot(t) - err := bot.OrderManager.Start(bot) - if err != nil { - t.Fatal(err) - } - bot.ServicesWG.Wait() - if !bot.OrderManager.Started() { - t.Fatal("Order manager not started") - } - return bot -} - -func TestOrdersGet(t *testing.T) { - bot := OrdersSetup(t) - if bot.OrderManager.orderStore.get() == nil { - t.Error("orderStore not established") - } -} - -func TestOrdersAdd(t *testing.T) { - bot := OrdersSetup(t) - err := bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: testExchange, - ID: "TestOrdersAdd", - }) - if err != nil { - t.Error(err) - } - err = bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: "testTest", - ID: "TestOrdersAdd", - }) - if err == nil { - t.Error("Expected error from non existent exchange") - } - - err = bot.OrderManager.orderStore.Add(nil) - if err == nil { - t.Error("Expected error from nil order") - } - - err = bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: testExchange, - ID: "TestOrdersAdd", - }) - if err == nil { - t.Error("Expected error re-adding order") - } -} - -func TestGetByInternalOrderID(t *testing.T) { - bot := OrdersSetup(t) - err := bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: testExchange, - ID: "TestGetByInternalOrderID", - InternalOrderID: "internalTest", - }) - if err != nil { - t.Error(err) - } - - o, err := bot.OrderManager.orderStore.GetByInternalOrderID("internalTest") - if err != nil { - t.Error(err) - } - if o == nil { - t.Fatal("Expected a matching order") - } - if o.ID != "TestGetByInternalOrderID" { - t.Error("Expected to retrieve order") - } - - _, err = bot.OrderManager.orderStore.GetByInternalOrderID("NoOrder") - if err != ErrOrderNotFound { - t.Error(err) - } -} - -func TestGetByExchange(t *testing.T) { - bot := OrdersSetup(t) - err := bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: testExchange, - ID: "TestGetByExchange", - InternalOrderID: "internalTestGetByExchange", - }) - if err != nil { - t.Error(err) - } - - err = bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: testExchange, - ID: "TestGetByExchange2", - InternalOrderID: "internalTestGetByExchange2", - }) - if err != nil { - t.Error(err) - } - - err = bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: fakePassExchange, - ID: "TestGetByExchange3", - InternalOrderID: "internalTest3", - }) - if err != nil { - t.Error(err) - } - var o []*order.Detail - o, err = bot.OrderManager.orderStore.GetByExchange(testExchange) - if err != nil { - t.Error(err) - } - if o == nil { - t.Error("Expected non nil response") - } - var o1Found, o2Found bool - for i := range o { - if o[i].ID == "TestGetByExchange" && o[i].Exchange == testExchange { - o1Found = true - } - if o[i].ID == "TestGetByExchange2" && o[i].Exchange == testExchange { - o2Found = true - } - } - if !o1Found || !o2Found { - t.Error("Expected orders 'TestGetByExchange' and 'TestGetByExchange2' to be returned") - } - - _, err = bot.OrderManager.orderStore.GetByInternalOrderID("NoOrder") - if err != ErrOrderNotFound { - t.Error(err) - } - err = bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: "thisWillFail", - }) - if err == nil { - t.Error("Expected exchange not found error") - } -} - -func TestGetByExchangeAndID(t *testing.T) { - bot := OrdersSetup(t) - err := bot.OrderManager.orderStore.Add(&order.Detail{ - Exchange: testExchange, - ID: "TestGetByExchangeAndID", - }) - if err != nil { - t.Error(err) - } - - o, err := bot.OrderManager.orderStore.GetByExchangeAndID(testExchange, "TestGetByExchangeAndID") - if err != nil { - t.Error(err) - } - if o.ID != "TestGetByExchangeAndID" { - t.Error("Expected to retrieve order") - } - - _, err = bot.OrderManager.orderStore.GetByExchangeAndID("", "TestGetByExchangeAndID") - if err != ErrExchangeNotFound { - t.Error(err) - } - - _, err = bot.OrderManager.orderStore.GetByExchangeAndID(testExchange, "") - if err != ErrOrderNotFound { - t.Error(err) - } -} - -func TestExists(t *testing.T) { - bot := OrdersSetup(t) - if bot.OrderManager.orderStore.exists(nil) { - t.Error("Expected false") - } - o := &order.Detail{ - Exchange: testExchange, - ID: "TestExists", - } - err := bot.OrderManager.orderStore.Add(o) - if err != nil { - t.Error(err) - } - b := bot.OrderManager.orderStore.exists(o) - if !b { - t.Error("Expected true") - } -} - -func TestCancelOrder(t *testing.T) { - bot := OrdersSetup(t) - err := bot.OrderManager.Cancel(nil) - if err == nil { - t.Error("Expected error due to empty order") - } - - err = bot.OrderManager.Cancel(&order.Cancel{}) - if err == nil { - t.Error("Expected error due to empty order") - } - - err = bot.OrderManager.Cancel(&order.Cancel{ - Exchange: testExchange, - }) - if err == nil { - t.Error("Expected error due to no order ID") - } - - err = bot.OrderManager.Cancel(&order.Cancel{ - ID: "ID", - }) - if err == nil { - t.Error("Expected error due to no Exchange") - } - - err = bot.OrderManager.Cancel(&order.Cancel{ - ID: "ID", - Exchange: testExchange, - AssetType: asset.Binary, - }) - if err == nil { - t.Error("Expected error due to bad asset type") - } - - o := &order.Detail{ - Exchange: fakePassExchange, - ID: "TestCancelOrder", - Status: order.New, - } - err = bot.OrderManager.orderStore.Add(o) - if err != nil { - t.Error(err) - } - - err = bot.OrderManager.Cancel(&order.Cancel{ - ID: "Unknown", - Exchange: fakePassExchange, - AssetType: asset.Spot, - }) - if err == nil { - t.Error("Expected error due to no order found") - } - - pair, err := currency.NewPairFromString("BTCUSD") - if err != nil { - t.Fatal(err) - } - - cancel := &order.Cancel{ - Exchange: fakePassExchange, - ID: "TestCancelOrder", - Side: order.Sell, - Status: order.New, - AssetType: asset.Spot, - Date: time.Now(), - Pair: pair, - } - err = bot.OrderManager.Cancel(cancel) - if err != nil { - t.Error(err) - } - - if o.Status != order.Cancelled { - t.Error("Failed to cancel") - } -} - -func TestGetOrderInfo(t *testing.T) { - bot := OrdersSetup(t) - _, err := bot.OrderManager.GetOrderInfo("", "", currency.Pair{}, "") - if err == nil { - t.Error("Expected error due to empty order") - } - - var result order.Detail - result, err = bot.OrderManager.GetOrderInfo(fakePassExchange, "1234", currency.Pair{}, "") - if err != nil { - t.Error(err) - } - if result.ID != "fakeOrder" { - t.Error("unexpected order returned") - } - - result, err = bot.OrderManager.GetOrderInfo(fakePassExchange, "1234", currency.Pair{}, "") - if err != nil { - t.Error(err) - } - if result.ID != "fakeOrder" { - t.Error("unexpected order returned") - } -} - -func TestCancelAllOrders(t *testing.T) { - bot := OrdersSetup(t) - o := &order.Detail{ - Exchange: fakePassExchange, - ID: "TestCancelAllOrders", - Status: order.New, - } - err := bot.OrderManager.orderStore.Add(o) - if err != nil { - t.Error(err) - } - - bot.OrderManager.CancelAllOrders([]string{"NotFound"}) - if o.Status == order.Cancelled { - t.Error("Order should not be cancelled") - } - - bot.OrderManager.CancelAllOrders([]string{fakePassExchange}) - if o.Status != order.Cancelled { - t.Error("Order should be cancelled") - } - - o.Status = order.New - bot.OrderManager.CancelAllOrders(nil) - if o.Status != order.New { - t.Error("Order should not be cancelled") - } -} - -func TestSubmit(t *testing.T) { - bot := OrdersSetup(t) - _, err := bot.OrderManager.Submit(nil) - if err == nil { - t.Error("Expected error from nil order") - } - - o := &order.Submit{ - Exchange: "", - ID: "FakePassingExchangeOrder", - Status: order.New, - Type: order.Market, - } - _, err = bot.OrderManager.Submit(o) - if err == nil { - t.Error("Expected error from empty exchange") - } - - o.Exchange = fakePassExchange - _, err = bot.OrderManager.Submit(o) - if err == nil { - t.Error("Expected error from validation") - } - - pair, err := currency.NewPairFromString("BTCUSD") - if err != nil { - t.Fatal(err) - } - - bot.OrderManager.cfg.EnforceLimitConfig = true - bot.OrderManager.cfg.AllowMarketOrders = false - o.Pair = pair - o.AssetType = asset.Spot - o.Side = order.Buy - o.Amount = 1 - o.Price = 1 - _, err = bot.OrderManager.Submit(o) - if err == nil { - t.Error("Expected fail due to order market type is not allowed") - } - bot.OrderManager.cfg.AllowMarketOrders = true - bot.OrderManager.cfg.LimitAmount = 1 - o.Amount = 2 - _, err = bot.OrderManager.Submit(o) - if err == nil { - t.Error("Expected fail due to order limit exceeds allowed limit") - } - bot.OrderManager.cfg.LimitAmount = 0 - bot.OrderManager.cfg.AllowedExchanges = []string{"fake"} - _, err = bot.OrderManager.Submit(o) - if err == nil { - t.Error("Expected fail due to order exchange not found in allowed list") - } - - failPair, err := currency.NewPairFromString("BTCAUD") - if err != nil { - t.Fatal(err) - } - - bot.OrderManager.cfg.AllowedExchanges = nil - bot.OrderManager.cfg.AllowedPairs = currency.Pairs{failPair} - _, err = bot.OrderManager.Submit(o) - if err == nil { - t.Error("Expected fail due to order pair not found in allowed list") - } - - bot.OrderManager.cfg.AllowedPairs = nil - _, err = bot.OrderManager.Submit(o) - if err != nil { - t.Error(err) - } - - o2, err := bot.OrderManager.orderStore.GetByExchangeAndID(fakePassExchange, "FakePassingExchangeOrder") - if err != nil { - t.Error(err) - } - if o2.InternalOrderID == "" { - t.Error("Failed to assign internal order id") - } -} - -func TestProcessOrders(t *testing.T) { - bot := OrdersSetup(t) - bot.OrderManager.processOrders() -} diff --git a/engine/orders_types.go b/engine/orders_types.go deleted file mode 100644 index 3aa32b64..00000000 --- a/engine/orders_types.go +++ /dev/null @@ -1,41 +0,0 @@ -package engine - -import ( - "errors" - "sync" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" -) - -var ( - errOrderIDCannotBeEmpty = errors.New("orderID cannot be empty") -) - -type orderManagerConfig struct { - EnforceLimitConfig bool - AllowMarketOrders bool - CancelOrdersOnShutdown bool - LimitAmount float64 - AllowedPairs currency.Pairs - AllowedExchanges []string - OrderSubmissionRetries int64 -} - -type orderStore struct { - m sync.RWMutex - Orders map[string][]*order.Detail - bot *Engine -} - -type orderManager struct { - started int32 - shutdown chan struct{} - orderStore orderStore - cfg orderManagerConfig -} - -type orderSubmitResponse struct { - order.SubmitResponse - InternalOrderID string -} diff --git a/engine/portfolio.go b/engine/portfolio.go deleted file mode 100644 index 1bf7ced1..00000000 --- a/engine/portfolio.go +++ /dev/null @@ -1,100 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "sync/atomic" - "time" - - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" - "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/portfolio" -) - -// vars for the fund manager package -var ( - PortfolioSleepDelay = time.Minute -) - -type portfolioManager struct { - started int32 - processing int32 - shutdown chan struct{} -} - -func (p *portfolioManager) Started() bool { - return atomic.LoadInt32(&p.started) == 1 -} - -func (p *portfolioManager) Start() error { - if atomic.AddInt32(&p.started, 1) != 1 { - return errors.New("portfolio manager already started") - } - - log.Debugln(log.PortfolioMgr, "Portfolio manager starting...") - Bot.Portfolio = &portfolio.Portfolio - Bot.Portfolio.Seed(Bot.Config.Portfolio) - p.shutdown = make(chan struct{}) - portfolio.Verbose = Bot.Settings.Verbose - - go p.run() - return nil -} -func (p *portfolioManager) Stop() error { - if atomic.LoadInt32(&p.started) == 0 { - return fmt.Errorf("portfolio manager %w", subsystem.ErrSubSystemNotStarted) - } - defer func() { - atomic.CompareAndSwapInt32(&p.started, 1, 0) - }() - - log.Debugln(log.PortfolioMgr, "Portfolio manager shutting down...") - close(p.shutdown) - return nil -} - -func (p *portfolioManager) run() { - log.Debugln(log.PortfolioMgr, "Portfolio manager started.") - Bot.ServicesWG.Add(1) - tick := time.NewTicker(Bot.Settings.PortfolioManagerDelay) - defer func() { - tick.Stop() - Bot.ServicesWG.Done() - log.Debugf(log.PortfolioMgr, "Portfolio manager shutdown.") - }() - - go p.processPortfolio() - for { - select { - case <-p.shutdown: - return - case <-tick.C: - go p.processPortfolio() - } - } -} - -func (p *portfolioManager) processPortfolio() { - if !atomic.CompareAndSwapInt32(&p.processing, 0, 1) { - return - } - pf := portfolio.GetPortfolio() - data := pf.GetPortfolioGroupedCoin() - for key, value := range data { - err := pf.UpdatePortfolio(value, key) - if err != nil { - log.Errorf(log.PortfolioMgr, - "PortfolioWatcher error %s for currency %s\n", - err, - key) - continue - } - - log.Debugf(log.PortfolioMgr, - "Portfolio manager: Successfully updated address balance for %s address(es) %s\n", - key, - value) - } - SeedExchangeAccountInfo(Bot.GetAllEnabledExchangeAccountInfo().Data) - atomic.CompareAndSwapInt32(&p.processing, 1, 0) -} diff --git a/engine/portfolio_manager.go b/engine/portfolio_manager.go new file mode 100644 index 00000000..6f219d58 --- /dev/null +++ b/engine/portfolio_manager.go @@ -0,0 +1,327 @@ +package engine + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/log" + "github.com/thrasher-corp/gocryptotrader/portfolio" +) + +// PortfolioManagerName is an exported subsystem name +const PortfolioManagerName = "portfolio" + +var ( + // PortfolioSleepDelay defines the default sleep time between portfolio manager runs + PortfolioSleepDelay = time.Minute +) + +// portfolioManager routinely retrieves a user's holdings through exchange APIs as well +// as through addresses provided in the config +type portfolioManager struct { + started int32 + processing int32 + portfolioManagerDelay time.Duration + exchangeManager *ExchangeManager + shutdown chan struct{} + base *portfolio.Base +} + +// setupPortfolioManager creates a new portfolio manager +func setupPortfolioManager(e *ExchangeManager, portfolioManagerDelay time.Duration, cfg *portfolio.Base) (*portfolioManager, error) { + if e == nil { + return nil, errNilExchangeManager + } + if portfolioManagerDelay <= 0 { + portfolioManagerDelay = PortfolioSleepDelay + } + if cfg == nil { + cfg = &portfolio.Base{Addresses: []portfolio.Address{}} + } + m := &portfolioManager{ + portfolioManagerDelay: portfolioManagerDelay, + exchangeManager: e, + shutdown: make(chan struct{}), + base: cfg, + } + return m, nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *portfolioManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Start runs the subsystem +func (m *portfolioManager) Start(wg *sync.WaitGroup) error { + if m == nil { + return fmt.Errorf("portfolio manager %w", ErrNilSubsystem) + } + if wg == nil { + return errNilWaitGroup + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return fmt.Errorf("portfolio manager %w", ErrSubSystemAlreadyStarted) + } + + log.Debugf(log.PortfolioMgr, "Portfolio manager %s", MsgSubSystemStarting) + m.shutdown = make(chan struct{}) + go m.run(wg) + return nil +} + +// Stop attempts to shutdown the subsystem +func (m *portfolioManager) Stop() error { + if m == nil { + return fmt.Errorf("portfolio manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 1, 0) { + return fmt.Errorf("portfolio manager %w", ErrSubSystemNotStarted) + } + defer func() { + atomic.CompareAndSwapInt32(&m.started, 1, 0) + }() + + log.Debugf(log.PortfolioMgr, "Portfolio manager %s", MsgSubSystemShuttingDown) + close(m.shutdown) + return nil +} + +// run periodically will check and update portfolio holdings +func (m *portfolioManager) run(wg *sync.WaitGroup) { + log.Debugln(log.PortfolioMgr, "Portfolio manager started.") + wg.Add(1) + tick := time.NewTicker(m.portfolioManagerDelay) + defer func() { + tick.Stop() + wg.Done() + log.Debugf(log.PortfolioMgr, "Portfolio manager shutdown.") + }() + + go m.processPortfolio() + for { + select { + case <-m.shutdown: + return + case <-tick.C: + go m.processPortfolio() + } + } +} + +// processPortfolio updates portfolio holdings +func (m *portfolioManager) processPortfolio() { + if !atomic.CompareAndSwapInt32(&m.processing, 0, 1) { + return + } + data := m.base.GetPortfolioGroupedCoin() + for key, value := range data { + err := m.base.UpdatePortfolio(value, key) + if err != nil { + log.Errorf(log.PortfolioMgr, + "PortfolioWatcher error %s for currency %s\n", + err, + key) + continue + } + + log.Debugf(log.PortfolioMgr, + "Portfolio manager: Successfully updated address balance for %s address(es) %s\n", + key, + value) + } + + d := m.getExchangeAccountInfo(m.exchangeManager.GetExchanges()) + m.seedExchangeAccountInfo(d) + atomic.CompareAndSwapInt32(&m.processing, 1, 0) +} + +// seedExchangeAccountInfo seeds account info +func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings) { + if len(accounts) == 0 { + return + } + for x := range accounts { + exchangeName := accounts[x].Exchange + var currencies []account.Balance + for y := range accounts[x].Accounts { + for z := range accounts[x].Accounts[y].Currencies { + var update bool + for i := range currencies { + if accounts[x].Accounts[y].Currencies[z].CurrencyName == currencies[i].CurrencyName { + currencies[i].Hold += accounts[x].Accounts[y].Currencies[z].Hold + currencies[i].TotalValue += accounts[x].Accounts[y].Currencies[z].TotalValue + update = true + } + } + + if update { + continue + } + + currencies = append(currencies, account.Balance{ + CurrencyName: accounts[x].Accounts[y].Currencies[z].CurrencyName, + TotalValue: accounts[x].Accounts[y].Currencies[z].TotalValue, + Hold: accounts[x].Accounts[y].Currencies[z].Hold, + }) + } + } + + for x := range currencies { + currencyName := currencies[x].CurrencyName + total := currencies[x].TotalValue + + if !m.base.ExchangeAddressExists(exchangeName, currencyName) { + if total <= 0 { + continue + } + + log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n", + exchangeName, + currencyName, + total, + portfolio.ExchangeAddress) + + m.base.Addresses = append( + m.base.Addresses, + portfolio.Address{Address: exchangeName, + CoinType: currencyName, + Balance: total, + Description: portfolio.ExchangeAddress}) + } else { + if total <= 0 { + log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n", + exchangeName, + currencyName) + m.base.RemoveExchangeAddress(exchangeName, currencyName) + } else { + balance, ok := m.base.GetAddressBalance(exchangeName, + portfolio.ExchangeAddress, + currencyName) + if !ok { + continue + } + + if balance != total { + log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n", + exchangeName, + currencyName, + total) + m.base.UpdateExchangeAddressBalance(exchangeName, + currencyName, + total) + } + } + } + } + } +} + +// getExchangeAccountInfo returns all the current enabled exchanges +func (m *portfolioManager) getExchangeAccountInfo(exchanges []exchange.IBotExchange) []account.Holdings { + var response []account.Holdings + for x := range exchanges { + if exchanges[x] == nil || !exchanges[x].IsEnabled() { + continue + } + if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) { + if m.base.Verbose { + log.Debugf(log.PortfolioMgr, + "skipping %s due to disabled authenticated API support.\n", + exchanges[x].GetName()) + } + continue + } + assetTypes := exchanges[x].GetAssetTypes() + var exchangeHoldings account.Holdings + for y := range assetTypes { + accountHoldings, err := exchanges[x].FetchAccountInfo(assetTypes[y]) + if err != nil { + log.Errorf(log.PortfolioMgr, + "Error encountered retrieving exchange account info for %s. Error %s\n", + exchanges[x].GetName(), + err) + continue + } + for z := range accountHoldings.Accounts { + accountHoldings.Accounts[z].AssetType = assetTypes[y] + } + exchangeHoldings.Exchange = exchanges[x].GetName() + exchangeHoldings.Accounts = append(exchangeHoldings.Accounts, accountHoldings.Accounts...) + } + response = append(response, exchangeHoldings) + } + return response +} + +// AddAddress adds a new portfolio address for the portfolio manager to track +func (m *portfolioManager) AddAddress(address, description string, coinType currency.Code, balance float64) error { + if m == nil { + return fmt.Errorf("portfolio manager %w", ErrNilSubsystem) + } + if !m.IsRunning() { + return fmt.Errorf("portfolio manager %w", ErrSubSystemNotStarted) + } + return m.base.AddAddress(address, description, coinType, balance) +} + +// RemoveAddress removes a portfolio address +func (m *portfolioManager) RemoveAddress(address, description string, coinType currency.Code) error { + if m == nil { + return fmt.Errorf("portfolio manager %w", ErrNilSubsystem) + } + if !m.IsRunning() { + return fmt.Errorf("portfolio manager %w", ErrSubSystemNotStarted) + } + return m.base.RemoveAddress(address, description, coinType) +} + +// GetPortfolioSummary returns a summary of all portfolio holdings +func (m *portfolioManager) GetPortfolioSummary() portfolio.Summary { + if m == nil || !m.IsRunning() { + return portfolio.Summary{} + } + return m.base.GetPortfolioSummary() +} + +// GetAddresses returns all addresses +func (m *portfolioManager) GetAddresses() []portfolio.Address { + if m == nil || !m.IsRunning() { + return nil + } + return m.base.Addresses +} + +// GetPortfolio returns a copy of the internal portfolio base for +// saving addresses to the config +func (m *portfolioManager) GetPortfolio() *portfolio.Base { + if m == nil || !m.IsRunning() { + return nil + } + resp := m.base + return resp +} + +// IsWhiteListed checks if an address is whitelisted to withdraw to +func (m *portfolioManager) IsWhiteListed(address string) bool { + if m == nil || !m.IsRunning() { + return false + } + return m.base.IsWhiteListed(address) +} + +// IsExchangeSupported checks if an exchange is supported +func (m *portfolioManager) IsExchangeSupported(exchange, address string) bool { + if m == nil || !m.IsRunning() { + return false + } + return m.base.IsExchangeSupported(exchange, address) +} diff --git a/engine/portfolio_manager.md b/engine/portfolio_manager.md new file mode 100644 index 00000000..fe3918f2 --- /dev/null +++ b/engine/portfolio_manager.md @@ -0,0 +1,66 @@ +# GoCryptoTrader package Portfolio_manager + + + + +[![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/portfolio_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This portfolio_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Portfolio_manager ++ The portfolio manager subsystem is used to synchronise and monitor wallet addresses ++ It can read addresses specified in your config file ++ If you have set API keys for an enabled exchange and enabled `authenticatedSupport`, it will store your exchange addresses ++ In order to modify the behaviour of the portfolio manager subsystem, you can edit the following inside your config file under `portfolioAddresses`: + +### portfolioAddresses + +| Config | Description | Example | +| ------ | ----------- | ------- | +| Verbose | Enabling this will output more detailed logs to your logging output | `false` | +| addresses | An array of portfolio wallet addresses to monitor, see below table | | + +### addresses + +| Config | Description | Example | +| ------ | ----------- | ------- | +| Address | The wallet address | `bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc` | +| CoinType | The coin for the wallet address | `BTC` | +| Balance | The balance of the wallet | | +| Description | A customisable description | `My secret billion stash` | +| WhiteListed | Determines whether GoCryptoTrader withdraw manager subsystem can make withdrawals from this address | `true` | +| ColdStorage | Describes whether the wallet address is a cold storage wallet eg Ledger | `false` | +| SupportedExchanges | A comma delimited string of which exchanges are allowed to interact with this wallet | `"Binance"` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/portfolio_manager_test.go b/engine/portfolio_manager_test.go new file mode 100644 index 00000000..40ff4776 --- /dev/null +++ b/engine/portfolio_manager_test.go @@ -0,0 +1,117 @@ +package engine + +import ( + "errors" + "sync" + "testing" +) + +func TestSetupPortfolioManager(t *testing.T) { + _, err := setupPortfolioManager(nil, 0, nil) + if !errors.Is(err, errNilExchangeManager) { + t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) + } + + m, err := setupPortfolioManager(SetupExchangeManager(), 0, nil) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expected manager") + } +} + +func TestIsPortfolioManagerRunning(t *testing.T) { + var m *portfolioManager + if m.IsRunning() { + t.Error("expected false") + } + + m, err := setupPortfolioManager(SetupExchangeManager(), 0, nil) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.IsRunning() { + t.Error("expected false") + } + var wg sync.WaitGroup + err = m.Start(&wg) + if err != nil { + t.Error(err) + } + if !m.IsRunning() { + t.Error("expected true") + } +} + +func TestPortfolioManagerStart(t *testing.T) { + var m *portfolioManager + var wg sync.WaitGroup + err := m.Start(nil) + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + m, err = setupPortfolioManager(SetupExchangeManager(), 0, nil) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Start(nil) + if !errors.Is(err, errNilWaitGroup) { + t.Errorf("error '%v', expected '%v'", err, errNilWaitGroup) + } + + err = m.Start(&wg) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Start(&wg) + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } +} + +func TestPortfolioManagerStop(t *testing.T) { + var m *portfolioManager + var wg sync.WaitGroup + err := m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + m, err = setupPortfolioManager(SetupExchangeManager(), 0, nil) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start(&wg) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestProcessPortfolio(t *testing.T) { + em := SetupExchangeManager() + exch, err := em.NewExchangeByName("Bitstamp") + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + exch.SetDefaults() + em.Add(exch) + m, err := setupPortfolioManager(em, 0, nil) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + m.processPortfolio() +} diff --git a/engine/restful_router.go b/engine/restful_router.go deleted file mode 100644 index b9648090..00000000 --- a/engine/restful_router.go +++ /dev/null @@ -1,118 +0,0 @@ -package engine - -import ( - "fmt" - "net/http" - "net/http/pprof" - "runtime" - "strconv" - "strings" - "time" - - "github.com/gorilla/mux" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// 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), - ) - }) -} - -// StartRESTServer starts a REST server -func StartRESTServer(bot *Engine) { - listenAddr := bot.Config.RemoteControl.DeprecatedRPC.ListenAddress - log.Debugf(log.RESTSys, - "Deprecated RPC server support enabled. Listen URL: http://%s:%d\n", - common.ExtractHost(listenAddr), common.ExtractPort(listenAddr)) - err := http.ListenAndServe(listenAddr, newRouter(bot, true)) - if err != nil { - log.Errorf(log.RESTSys, "Failed to start deprecated RPC server. Err: %s", err) - } -} - -// StartWebsocketServer starts a Websocket server -func StartWebsocketServer(bot *Engine) { - listenAddr := bot.Config.RemoteControl.WebsocketRPC.ListenAddress - log.Debugf(log.RESTSys, - "Websocket RPC support enabled. Listen URL: ws://%s:%d/ws\n", - common.ExtractHost(listenAddr), common.ExtractPort(listenAddr)) - err := http.ListenAndServe(listenAddr, newRouter(bot, false)) - if err != nil { - log.Errorf(log.RESTSys, "Failed to start websocket RPC server. Err: %s", err) - } -} - -// newRouter takes in the exchange interfaces and returns a new multiplexor -// router -func newRouter(bot *Engine, isREST bool) *mux.Router { - router := mux.NewRouter().StrictSlash(true) - var routes []Route - var listenAddr string - - if isREST { - listenAddr = bot.Config.RemoteControl.DeprecatedRPC.ListenAddress - } else { - listenAddr = bot.Config.RemoteControl.WebsocketRPC.ListenAddress - } - - if common.ExtractPort(listenAddr) == 80 { - listenAddr = common.ExtractHost(listenAddr) - } else { - listenAddr = strings.Join([]string{common.ExtractHost(listenAddr), - strconv.Itoa(common.ExtractPort(listenAddr))}, ":") - } - - if isREST { - routes = []Route{ - {"", http.MethodGet, "/", getIndex}, - {"GetAllSettings", http.MethodGet, "/config/all", RESTGetAllSettings}, - {"SaveAllSettings", http.MethodPost, "/config/all/save", RESTSaveAllSettings}, - {"AllEnabledAccountInfo", http.MethodGet, "/exchanges/enabled/accounts/all", RESTGetAllEnabledAccountInfo}, - {"AllActiveExchangesAndCurrencies", http.MethodGet, "/exchanges/enabled/latest/all", RESTGetAllActiveTickers}, - {"GetPortfolio", http.MethodGet, "/portfolio/all", RESTGetPortfolio}, - {"AllActiveExchangesAndOrderbooks", http.MethodGet, "/exchanges/orderbook/latest/all", RESTGetAllActiveOrderbooks}, - } - - if bot.Config.Profiler.Enabled { - if bot.Config.Profiler.MutexProfileFraction > 0 { - runtime.SetMutexProfileFraction(bot.Config.Profiler.MutexProfileFraction) - } - log.Debugf(log.RESTSys, - "HTTP Go performance profiler (pprof) endpoint enabled: http://%s:%d/debug/pprof/\n", - common.ExtractHost(listenAddr), - common.ExtractPort(listenAddr)) - router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) - } - } else { - routes = []Route{ - {"ws", http.MethodGet, "/ws", WebsocketClientHandler}, - } - } - - for _, route := range routes { - router. - Methods(route.Method). - Path(route.Pattern). - Name(route.Name). - Handler(RESTLogger(route.HandlerFunc, route.Name)). - Host(listenAddr) - } - return router -} - -func getIndex(w http.ResponseWriter, _ *http.Request) { - fmt.Fprint(w, "GoCryptoTrader RESTful interface. For the web GUI, please visit the web GUI readme.") - w.WriteHeader(http.StatusOK) -} diff --git a/engine/restful_server.go b/engine/restful_server.go deleted file mode 100644 index da0b03cc..00000000 --- a/engine/restful_server.go +++ /dev/null @@ -1,135 +0,0 @@ -package engine - -import ( - "encoding/json" - "net/http" - - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/portfolio" -) - -// RESTfulJSONResponse outputs a JSON response of the response interface -func RESTfulJSONResponse(w http.ResponseWriter, response interface{}) error { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - return json.NewEncoder(w).Encode(response) -} - -// RESTfulError prints the REST method and error -func RESTfulError(method string, err error) { - log.Errorf(log.RESTSys, "RESTful %s: server 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 RESTGetAllSettings(w http.ResponseWriter, r *http.Request) { - err := RESTfulJSONResponse(w, config.Cfg) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTSaveAllSettings saves all current settings from request body as a JSON -// document then reloads state and returns the settings -func 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 { - RESTfulError(r.Method, err) - } - // Save change the settings - err = Bot.Config.UpdateConfig(Bot.Settings.ConfigFile, &responseData.Data, false) - if err != nil { - RESTfulError(r.Method, err) - } - - err = RESTfulJSONResponse(w, Bot.Config) - if err != nil { - RESTfulError(r.Method, err) - } - - Bot.SetupExchanges() -} - -// GetAllActiveOrderbooks returns all enabled exchanges orderbooks -func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks { - var orderbookData []EnabledExchangeOrderbooks - exchanges := Bot.GetExchanges() - for x := range exchanges { - assets := exchanges[x].GetAssetTypes() - exchName := exchanges[x].GetName() - var exchangeOB EnabledExchangeOrderbooks - exchangeOB.ExchangeName = exchName - - for y := range assets { - currencies, err := exchanges[x].GetEnabledPairs(assets[y]) - if err != nil { - log.Errorf(log.RESTSys, - "Exchange %s could not retrieve enabled currencies. Err: %s\n", - exchName, - err) - continue - } - for z := range currencies { - ob, err := exchanges[x].FetchOrderbook(currencies[z], assets[y]) - if err != nil { - log.Errorf(log.RESTSys, - "Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName, - currencies[z].String(), - err) - continue - } - exchangeOB.ExchangeValues = append(exchangeOB.ExchangeValues, *ob) - } - orderbookData = append(orderbookData, exchangeOB) - } - orderbookData = append(orderbookData, exchangeOB) - } - return orderbookData -} - -// RESTGetAllActiveOrderbooks returns all enabled exchange orderbooks -func RESTGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeOrderbooks - response.Data = GetAllActiveOrderbooks() - - err := RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTGetPortfolio returns the Bot portfolio -func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { - p := portfolio.GetPortfolio() - result := p.GetPortfolioSummary() - err := RESTfulJSONResponse(w, result) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTGetAllActiveTickers returns all active tickers -func RESTGetAllActiveTickers(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeCurrencies - response.Data = Bot.GetAllActiveTickers() - - err := RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTGetAllEnabledAccountInfo via get request returns JSON response of account -// info -func RESTGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { - response := Bot.GetAllEnabledExchangeAccountInfo() - err := RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} diff --git a/engine/restful_server_test.go b/engine/restful_server_test.go deleted file mode 100644 index 7501ab3b..00000000 --- a/engine/restful_server_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package engine - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "reflect" - "runtime" - "testing" - - "github.com/thrasher-corp/gocryptotrader/config" -) - -func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response { - w := httptest.NewRecorder() - - err := RESTfulJSONResponse(w, response) - if err != nil { - t.Error("Failed to make response.", err) - } - return w.Result() -} - -// TestConfigAllJsonResponse test if config/all restful json response is valid -func TestConfigAllJsonResponse(t *testing.T) { - bot := CreateTestBot(t) - resp := makeHTTPGetRequest(t, bot.Config) - body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - t.Error("Body not readable", err) - } - - var responseConfig config.Config - jsonErr := json.Unmarshal(body, &responseConfig) - if jsonErr != nil { - t.Error("Response not parseable as json", err) - } - - if reflect.DeepEqual(responseConfig, bot.Config) { - t.Error("Json not equal to config") - } -} - -func TestInvalidHostRequest(t *testing.T) { - e := CreateTestBot(t) - req, err := http.NewRequest(http.MethodGet, "/config/all", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "invalidsite.com" - - resp := httptest.NewRecorder() - newRouter(e, true).ServeHTTP(resp, req) - - if status := resp.Code; status != http.StatusNotFound { - t.Errorf("Response returned wrong status code expected %v got %v", http.StatusNotFound, status) - } -} - -func TestValidHostRequest(t *testing.T) { - e := CreateTestBot(t) - if config.Cfg.Name == "" { - config.Cfg = *e.Config - } - req, err := http.NewRequest(http.MethodGet, "/config/all", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "localhost:9050" - - resp := httptest.NewRecorder() - newRouter(e, true).ServeHTTP(resp, req) - - if status := resp.Code; status != http.StatusOK { - t.Errorf("Response returned wrong status code expected %v got %v", http.StatusOK, status) - } -} - -func TestProfilerEnabledShouldEnableProfileEndPoint(t *testing.T) { - e := CreateTestBot(t) - req, err := http.NewRequest(http.MethodGet, "/debug/pprof/", nil) - if err != nil { - t.Fatal(err) - } - - req.Host = "localhost:9050" - resp := httptest.NewRecorder() - newRouter(e, true).ServeHTTP(resp, req) - if status := resp.Code; status != http.StatusNotFound { - t.Errorf("Response returned wrong status code expected %v got %v", http.StatusNotFound, status) - } - - e.Config.Profiler.Enabled = true - e.Config.Profiler.MutexProfileFraction = 5 - req, err = http.NewRequest(http.MethodGet, "/debug/pprof/", nil) - if err != nil { - t.Fatal(err) - } - - mutexValue := runtime.SetMutexProfileFraction(10) - if mutexValue != 0 { - t.Fatalf("SetMutexProfileFraction() should be 0 on first set received: %v", mutexValue) - } - - resp = httptest.NewRecorder() - newRouter(e, true).ServeHTTP(resp, req) - mutexValue = runtime.SetMutexProfileFraction(10) - if mutexValue != 5 { - t.Fatalf("SetMutexProfileFraction() should be 5 after setup received: %v", mutexValue) - } - if status := resp.Code; status != http.StatusOK { - t.Errorf("Response returned wrong status code expected %v got %v", http.StatusOK, status) - } -} diff --git a/engine/restful_types.go b/engine/restful_types.go deleted file mode 100644 index 31a3ec3e..00000000 --- a/engine/restful_types.go +++ /dev/null @@ -1,46 +0,0 @@ -package engine - -import ( - "net/http" - - "github.com/thrasher-corp/gocryptotrader/exchanges/account" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" -) - -// 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.Base `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"` -} diff --git a/engine/routines.go b/engine/routines.go deleted file mode 100644 index ba396df2..00000000 --- a/engine/routines.go +++ /dev/null @@ -1,427 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "strconv" - "strings" - "sync" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/stats" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/log" -) - -func printCurrencyFormat(price float64, displayCurrency currency.Code) string { - displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) - if err != nil { - log.Errorf(log.Global, "Failed to get display symbol: %s\n", err) - } - - return fmt.Sprintf("%s%.8f", displaySymbol, price) -} - -func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64, displayCurrency currency.Code) string { - conv, err := currency.ConvertCurrency(origPrice, - origCurrency, - displayCurrency) - if err != nil { - log.Errorf(log.Global, "Failed to convert currency: %s\n", err) - } - - displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) - if err != nil { - log.Errorf(log.Global, "Failed to get display symbol: %s\n", err) - } - - origSymbol, err := currency.GetSymbolByCurrencyName(origCurrency) - if err != nil { - log.Errorf(log.Global, "Failed to get original currency symbol for %s: %s\n", - origCurrency, - err) - } - - return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", - displaySymbol, - conv, - displayCurrency, - origSymbol, - origPrice, - origCurrency, - ) -} - -func printTickerSummary(result *ticker.Price, protocol string, err error) { - if err != nil { - if err == common.ErrNotYetImplemented { - log.Warnf(log.Ticker, "Failed to get %s ticker. Error: %s\n", - protocol, - err) - return - } - log.Errorf(log.Ticker, "Failed to get %s ticker. Error: %s\n", - protocol, - err) - return - } - - stats.Add(result.ExchangeName, result.Pair, result.AssetType, result.Last, result.Volume) - if result.Pair.Quote.IsFiatCurrency() && - Bot != nil && - result.Pair.Quote != Bot.Config.Currency.FiatDisplayCurrency { - origCurrency := result.Pair.Quote.Upper() - log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n", - result.ExchangeName, - protocol, - Bot.FormatCurrency(result.Pair), - strings.ToUpper(result.AssetType.String()), - printConvertCurrencyFormat(origCurrency, result.Last, Bot.Config.Currency.FiatDisplayCurrency), - printConvertCurrencyFormat(origCurrency, result.Ask, Bot.Config.Currency.FiatDisplayCurrency), - printConvertCurrencyFormat(origCurrency, result.Bid, Bot.Config.Currency.FiatDisplayCurrency), - printConvertCurrencyFormat(origCurrency, result.High, Bot.Config.Currency.FiatDisplayCurrency), - printConvertCurrencyFormat(origCurrency, result.Low, Bot.Config.Currency.FiatDisplayCurrency), - result.Volume) - } else { - if result.Pair.Quote.IsFiatCurrency() && - Bot != nil && - result.Pair.Quote == Bot.Config.Currency.FiatDisplayCurrency { - log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n", - result.ExchangeName, - protocol, - Bot.FormatCurrency(result.Pair), - strings.ToUpper(result.AssetType.String()), - printCurrencyFormat(result.Last, Bot.Config.Currency.FiatDisplayCurrency), - printCurrencyFormat(result.Ask, Bot.Config.Currency.FiatDisplayCurrency), - printCurrencyFormat(result.Bid, Bot.Config.Currency.FiatDisplayCurrency), - printCurrencyFormat(result.High, Bot.Config.Currency.FiatDisplayCurrency), - printCurrencyFormat(result.Low, Bot.Config.Currency.FiatDisplayCurrency), - result.Volume) - } else { - log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f\n", - result.ExchangeName, - protocol, - Bot.FormatCurrency(result.Pair), - strings.ToUpper(result.AssetType.String()), - result.Last, - result.Ask, - result.Bid, - result.High, - result.Low, - result.Volume) - } - } -} - -const ( - book = "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n" -) - -func printOrderbookSummary(result *orderbook.Base, protocol string, bot *Engine, err error) { - if err != nil { - if result == nil { - log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n", - protocol, - err) - return - } - if err == common.ErrNotYetImplemented { - log.Warnf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n", - protocol, - result.Exchange, - result.Pair, - result.Asset, - err) - return - } - log.Errorf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n", - protocol, - result.Exchange, - result.Pair, - result.Asset, - err) - return - } - - bidsAmount, bidsValue := result.TotalBidsAmount() - asksAmount, asksValue := result.TotalAsksAmount() - - var bidValueResult, askValueResult string - switch { - case result.Pair.Quote.IsFiatCurrency() && bot != nil && result.Pair.Quote != bot.Config.Currency.FiatDisplayCurrency: - origCurrency := result.Pair.Quote.Upper() - bidValueResult = printConvertCurrencyFormat(origCurrency, bidsValue, bot.Config.Currency.FiatDisplayCurrency) - askValueResult = printConvertCurrencyFormat(origCurrency, asksValue, bot.Config.Currency.FiatDisplayCurrency) - case result.Pair.Quote.IsFiatCurrency() && bot != nil && result.Pair.Quote == bot.Config.Currency.FiatDisplayCurrency: - bidValueResult = printCurrencyFormat(bidsValue, bot.Config.Currency.FiatDisplayCurrency) - askValueResult = printCurrencyFormat(asksValue, bot.Config.Currency.FiatDisplayCurrency) - default: - bidValueResult = strconv.FormatFloat(bidsValue, 'f', -1, 64) - askValueResult = strconv.FormatFloat(asksValue, 'f', -1, 64) - } - - log.Infof(log.OrderBook, book, - result.Exchange, - protocol, - bot.FormatCurrency(result.Pair), - strings.ToUpper(result.Asset.String()), - len(result.Bids), - bidsAmount, - result.Pair.Base, - bidValueResult, - len(result.Asks), - asksAmount, - result.Pair.Base, - askValueResult, - ) -} - -func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { - evt := WebsocketEvent{ - Data: result, - Event: event, - AssetType: assetType, - Exchange: exchangeName, - } - err := BroadcastWebsocketMessage(evt) - if err != nil { - log.Errorf(log.WebsocketMgr, "Failed to broadcast websocket event %v. Error: %s\n", - event, err) - } -} - -// WebsocketRoutine Initial routine management system for websocket -func (bot *Engine) WebsocketRoutine() { - if bot.Settings.Verbose { - log.Debugln(log.WebsocketMgr, "Connecting exchange websocket services...") - } - - exchanges := bot.GetExchanges() - for i := range exchanges { - go func(i int) { - if exchanges[i].SupportsWebsocket() { - if bot.Settings.Verbose { - log.Debugf(log.WebsocketMgr, - "Exchange %s websocket support: Yes Enabled: %v\n", - exchanges[i].GetName(), - common.IsEnabled(exchanges[i].IsWebsocketEnabled()), - ) - } - - ws, err := exchanges[i].GetWebsocket() - if err != nil { - log.Errorf( - log.WebsocketMgr, - "Exchange %s GetWebsocket error: %s\n", - exchanges[i].GetName(), - err, - ) - return - } - - // Exchange sync manager might have already started ws - // service or is in the process of connecting, so check - if ws.IsConnected() || ws.IsConnecting() { - return - } - - // Data handler routine - go bot.WebsocketDataReceiver(ws) - - if ws.IsEnabled() { - err = ws.Connect() - if err != nil { - log.Errorf(log.WebsocketMgr, "%v\n", err) - } - err = ws.FlushChannels() - if err != nil { - log.Errorf(log.WebsocketMgr, "Failed to subscribe: %v\n", err) - } - } - } else if bot.Settings.Verbose { - log.Debugf(log.WebsocketMgr, - "Exchange %s websocket support: No\n", - exchanges[i].GetName(), - ) - } - }(i) - } -} - -var shutdowner = make(chan struct{}, 1) -var wg sync.WaitGroup - -// WebsocketDataReceiver handles websocket data coming from a websocket feed -// associated with an exchange -func (bot *Engine) WebsocketDataReceiver(ws *stream.Websocket) { - wg.Add(1) - defer wg.Done() - - for { - select { - case <-shutdowner: - return - case data := <-ws.ToRoutine: - err := bot.WebsocketDataHandler(ws.GetName(), data) - if err != nil { - log.Error(log.WebsocketMgr, err) - } - } - } -} - -// WebsocketDataHandler is a central point for exchange websocket implementations to send -// processed data. WebsocketDataHandler will then pass that to an appropriate handler -func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error { - if data == nil { - return fmt.Errorf("routines.go - exchange %s nil data sent to websocket", - exchName) - } - - switch d := data.(type) { - case string: - log.Info(log.WebsocketMgr, d) - case error: - return fmt.Errorf("routines.go exchange %s websocket error - %s", exchName, data) - case stream.FundingData: - if bot.Settings.Verbose { - log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v", - exchName, - bot.FormatCurrency(d.CurrencyPair), - d.AssetType, - d) - } - case *ticker.Price: - if bot.Settings.EnableExchangeSyncManager && bot.ExchangeCurrencyPairManager != nil { - bot.ExchangeCurrencyPairManager.update(exchName, - d.Pair, - d.AssetType, - SyncItemTicker, - nil) - } - err := ticker.ProcessTicker(d) - printTickerSummary(d, "websocket", err) - case stream.KlineData: - if bot.Settings.Verbose { - log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", - exchName, - bot.FormatCurrency(d.Pair), - d.AssetType, - d) - } - case *orderbook.Base: - if bot.Settings.EnableExchangeSyncManager && bot.ExchangeCurrencyPairManager != nil { - bot.ExchangeCurrencyPairManager.update(exchName, - d.Pair, - d.Asset, - SyncItemOrderbook, - nil) - } - printOrderbookSummary(d, "websocket", bot, nil) - case *order.Detail: - if bot.Settings.Verbose { - printOrderSummary(d) - } - // TODO: Dont check if exists this creates two locks, on conflict update - // else insert. - if !bot.OrderManager.orderStore.exists(d) { - err := bot.OrderManager.orderStore.Add(d) - if err != nil { - return err - } - } else { - od, err := bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID) - if err != nil { - return err - } - od.UpdateOrderFromDetail(d) - } - case *order.Modify: - if bot.Settings.Verbose { - printOrderChangeSummary(d) - } - // TODO: On conflict update or insert if not found - od, err := bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID) - if err != nil { - return err - } - od.UpdateOrderFromModify(d) - case order.ClassificationError: - return errors.New(d.Error()) - case stream.UnhandledMessageWarning: - log.Warn(log.WebsocketMgr, d.Message) - case account.Change: - if bot.Settings.Verbose { - printAccountHoldingsChangeSummary(d) - } - default: - if bot.Settings.Verbose { - log.Warnf(log.WebsocketMgr, - "%s websocket Unknown type: %+v", - exchName, - d) - } - } - return nil -} - -// printOrderChangeSummary this function will be deprecated when a order manager -// update is done. -func printOrderChangeSummary(m *order.Modify) { - if m == nil { - return - } - log.Debugf(log.WebsocketMgr, - "Order Change: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f", - m.Exchange, - m.AssetType, - m.Pair, - m.Status, - m.Type, - m.Side, - m.ID, - m.ClientOrderID, - m.Price, - m.Amount, - m.ExecutedAmount, - m.RemainingAmount) -} - -// printOrderSummary this function will be deprecated when a order manager -// update is done. -func printOrderSummary(m *order.Detail) { - if m == nil { - return - } - log.Debugf(log.WebsocketMgr, - "New Order: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f", - m.Exchange, - m.AssetType, - m.Pair, - m.Status, - m.Type, - m.Side, - m.ID, - m.ClientOrderID, - m.Price, - m.Amount, - m.ExecutedAmount, - m.RemainingAmount) -} - -// printAccountHoldingsChangeSummary this function will be deprecated when a -// account holdings update is done. -func printAccountHoldingsChangeSummary(m account.Change) { - log.Debugf(log.WebsocketMgr, - "Account Holdings Balance Changed: %s %s %s has changed balance by %f for account: %s", - m.Exchange, - m.Asset, - m.Currency, - m.Amount, - m.Account) -} diff --git a/engine/routines_test.go b/engine/routines_test.go deleted file mode 100644 index 7f7a4284..00000000 --- a/engine/routines_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package engine - -import ( - "errors" - "testing" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" -) - -func TestWebsocketDataHandlerProcess(t *testing.T) { - ws := sharedtestvalues.NewTestWebsocket() - b := OrdersSetup(t) - go b.WebsocketDataReceiver(ws) - ws.DataHandler <- "string" - time.Sleep(time.Second) - close(shutdowner) -} - -func TestHandleData(t *testing.T) { - b := OrdersSetup(t) - var exchName = "exch" - var orderID = "testOrder.Detail" - err := b.WebsocketDataHandler(exchName, errors.New("error")) - if err == nil { - t.Error("Error not handled correctly") - } - err = b.WebsocketDataHandler(exchName, nil) - if err == nil { - t.Error("Expected nil data error") - } - err = b.WebsocketDataHandler(exchName, stream.FundingData{}) - if err != nil { - t.Error(err) - } - err = b.WebsocketDataHandler(exchName, &ticker.Price{}) - if err != nil { - t.Error(err) - } - err = b.WebsocketDataHandler(exchName, stream.KlineData{}) - if err != nil { - t.Error(err) - } - origOrder := &order.Detail{ - Exchange: fakePassExchange, - ID: orderID, - Amount: 1337, - Price: 1337, - } - err = b.WebsocketDataHandler(exchName, origOrder) - if err != nil { - t.Error(err) - } - // Send it again since it exists now - err = b.WebsocketDataHandler(exchName, &order.Detail{ - Exchange: fakePassExchange, - ID: orderID, - Amount: 1338, - }) - if err != nil { - t.Error(err) - } - if origOrder.Amount != 1338 { - t.Error("Bad pipeline") - } - - err = b.WebsocketDataHandler(exchName, &order.Modify{ - Exchange: fakePassExchange, - ID: orderID, - Status: order.Active, - }) - if err != nil { - t.Error(err) - } - if origOrder.Status != order.Active { - t.Error("Expected order to be modified to Active") - } - - // Send some gibberish - err = b.WebsocketDataHandler(exchName, order.Stop) - if err != nil { - t.Error(err) - } - - err = b.WebsocketDataHandler(exchName, stream.UnhandledMessageWarning{ - Message: "there's an issue here's a tissue"}, - ) - if err != nil { - t.Error(err) - } - - classificationError := order.ClassificationError{ - Exchange: "test", - OrderID: "one", - Err: errors.New("lol"), - } - err = b.WebsocketDataHandler(exchName, classificationError) - if err == nil { - t.Error("Expected error") - } - if err != nil && err.Error() != classificationError.Error() { - t.Errorf("Problem formatting error. Expected %v Received %v", classificationError.Error(), err.Error()) - } - - err = b.WebsocketDataHandler(exchName, &orderbook.Base{ - Exchange: fakePassExchange, - Pair: currency.NewPair(currency.BTC, currency.USD), - }) - if err != nil { - t.Error(err) - } - err = b.WebsocketDataHandler(exchName, "this is a test string") - if err != nil { - t.Error(err) - } -} diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 00133d92..c811536a 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -62,15 +62,16 @@ var ( errDispatchSystem = errors.New("dispatch system offline") errCurrencyNotEnabled = errors.New("currency not enabled") errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list") + errNoTrades = errors.New("no trades returned from supplied params") ) // RPCServer struct type RPCServer struct { - *Engine gctrpc.UnimplementedGoCryptoTraderServer + *Engine } -func (bot *Engine) authenticateClient(ctx context.Context) (context.Context, error) { +func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return ctx, fmt.Errorf("unable to extract metadata") @@ -93,7 +94,7 @@ func (bot *Engine) authenticateClient(ctx context.Context) (context.Context, err username := strings.Split(string(decoded), ":")[0] password := strings.Split(string(decoded), ":")[1] - if username != bot.Config.RemoteControl.Username || password != bot.Config.RemoteControl.Password { + if username != s.Config.RemoteControl.Username || password != s.Config.RemoteControl.Password { return ctx, fmt.Errorf("username/password mismatch") } @@ -108,7 +109,6 @@ func StartRPCServer(engine *Engine) { log.Errorf(log.GRPCSys, "gRPC checkCerts failed. err: %s\n", err) return } - log.Debugf(log.GRPCSys, "gRPC server support enabled. Starting gRPC server on https://%v.\n", engine.Config.RemoteControl.GRPC.ListenAddress) lis, err := net.Listen("tcp", engine.Config.RemoteControl.GRPC.ListenAddress) if err != nil { @@ -122,12 +122,12 @@ func StartRPCServer(engine *Engine) { return } + s := RPCServer{Engine: engine} opts := []grpc.ServerOption{ grpc.Creds(creds), - grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(engine.authenticateClient)), + grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(s.authenticateClient)), } server := grpc.NewServer(opts...) - s := RPCServer{Engine: engine} gctrpc.RegisterGoCryptoTraderServer(server, &s) go func() { @@ -180,8 +180,8 @@ func (s *RPCServer) StartRPCRESTProxy() { } // GetInfo returns info about the current GoCryptoTrader session -func (s *RPCServer) GetInfo(_ context.Context, r *gctrpc.GetInfoRequest) (*gctrpc.GetInfoResponse, error) { - d := time.Since(s.Uptime) +func (s *RPCServer) GetInfo(_ context.Context, _ *gctrpc.GetInfoRequest) (*gctrpc.GetInfoResponse, error) { + d := time.Since(s.uptime) resp := gctrpc.GetInfoResponse{ Uptime: d.String(), EnabledExchanges: int64(s.Config.CountEnabledExchanges()), @@ -202,7 +202,7 @@ func (s *RPCServer) GetInfo(_ context.Context, r *gctrpc.GetInfoRequest) (*gctrp } // GetSubsystems returns a list of subsystems and their status -func (s *RPCServer) GetSubsystems(_ context.Context, r *gctrpc.GetSubsystemsRequest) (*gctrpc.GetSusbsytemsResponse, error) { +func (s *RPCServer) GetSubsystems(_ context.Context, _ *gctrpc.GetSubsystemsRequest) (*gctrpc.GetSusbsytemsResponse, error) { return &gctrpc.GetSusbsytemsResponse{SubsystemsStatus: s.GetSubsystemsStatus()}, nil } @@ -227,7 +227,7 @@ func (s *RPCServer) DisableSubsystem(_ context.Context, r *gctrpc.GenericSubsyst } // GetRPCEndpoints returns a list of API endpoints -func (s *RPCServer) GetRPCEndpoints(_ context.Context, r *gctrpc.GetRPCEndpointsRequest) (*gctrpc.GetRPCEndpointsResponse, error) { +func (s *RPCServer) GetRPCEndpoints(_ context.Context, _ *gctrpc.GetRPCEndpointsRequest) (*gctrpc.GetRPCEndpointsResponse, error) { endpoints := GetRPCEndpoints() var resp gctrpc.GetRPCEndpointsResponse resp.Endpoints = make(map[string]*gctrpc.RPCEndpoint) @@ -241,8 +241,8 @@ func (s *RPCServer) GetRPCEndpoints(_ context.Context, r *gctrpc.GetRPCEndpoints } // GetCommunicationRelayers returns the status of the engines communication relayers -func (s *RPCServer) GetCommunicationRelayers(_ context.Context, r *gctrpc.GetCommunicationRelayersRequest) (*gctrpc.GetCommunicationRelayersResponse, error) { - relayers, err := s.CommsManager.GetStatus() +func (s *RPCServer) GetCommunicationRelayers(_ context.Context, _ *gctrpc.GetCommunicationRelayersRequest) (*gctrpc.GetCommunicationRelayersResponse, error) { + relayers, err := s.CommunicationsManager.GetStatus() if err != nil { return nil, err } @@ -285,13 +285,13 @@ func (s *RPCServer) EnableExchange(_ context.Context, r *gctrpc.GenericExchangeN // GetExchangeOTPCode retrieves an exchanges OTP code func (s *RPCServer) GetExchangeOTPCode(_ context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GetExchangeOTPReponse, error) { - result, err := s.GetExchangeoOTPByName(r.Exchange) + result, err := s.GetExchangeOTPByName(r.Exchange) return &gctrpc.GetExchangeOTPReponse{OtpCode: result}, err } // GetExchangeOTPCodes retrieves OTP codes for all exchanges which have an // OTP secret installed -func (s *RPCServer) GetExchangeOTPCodes(_ context.Context, r *gctrpc.GetExchangeOTPsRequest) (*gctrpc.GetExchangeOTPsResponse, error) { +func (s *RPCServer) GetExchangeOTPCodes(_ context.Context, _ *gctrpc.GetExchangeOTPsRequest) (*gctrpc.GetExchangeOTPsResponse, error) { result, err := s.GetExchangeOTPs() return &gctrpc.GetExchangeOTPsResponse{OtpCodes: result}, err } @@ -377,32 +377,33 @@ func (s *RPCServer) GetTicker(_ context.Context, r *gctrpc.GetTickerRequest) (*g // GetTickers returns a list of tickers for all enabled exchanges and all // enabled currency pairs -func (s *RPCServer) GetTickers(_ context.Context, r *gctrpc.GetTickersRequest) (*gctrpc.GetTickersResponse, error) { +func (s *RPCServer) GetTickers(_ context.Context, _ *gctrpc.GetTickersRequest) (*gctrpc.GetTickersResponse, error) { activeTickers := s.GetAllActiveTickers() var tickers []*gctrpc.Tickers for x := range activeTickers { - var ticker gctrpc.Tickers - ticker.Exchange = activeTickers[x].ExchangeName + t := &gctrpc.Tickers{ + Exchange: activeTickers[x].ExchangeName, + } for y := range activeTickers[x].ExchangeValues { - t := activeTickers[x].ExchangeValues[y] - ticker.Tickers = append(ticker.Tickers, &gctrpc.TickerResponse{ + val := activeTickers[x].ExchangeValues[y] + t.Tickers = append(t.Tickers, &gctrpc.TickerResponse{ Pair: &gctrpc.CurrencyPair{ - Delimiter: t.Pair.Delimiter, - Base: t.Pair.Base.String(), - Quote: t.Pair.Quote.String(), + Delimiter: val.Pair.Delimiter, + Base: val.Pair.Base.String(), + Quote: val.Pair.Quote.String(), }, - LastUpdated: t.LastUpdated.Unix(), - Last: t.Last, - High: t.High, - Low: t.Low, - Bid: t.Bid, - Ask: t.Ask, - Volume: t.Volume, - PriceAth: t.PriceATH, + LastUpdated: val.LastUpdated.Unix(), + Last: val.Last, + High: val.High, + Low: val.Low, + Bid: val.Bid, + Ask: val.Ask, + Volume: val.Volume, + PriceAth: val.PriceATH, }) } - tickers = append(tickers, &ticker) + tickers = append(tickers, t) } return &gctrpc.GetTickersResponse{Tickers: tickers}, nil @@ -463,46 +464,66 @@ func (s *RPCServer) GetOrderbook(_ context.Context, r *gctrpc.GetOrderbookReques // GetOrderbooks returns a list of orderbooks for all enabled exchanges and all // enabled currency pairs -func (s *RPCServer) GetOrderbooks(_ context.Context, r *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) { - activeOrderbooks := GetAllActiveOrderbooks() - var orderbooks []*gctrpc.Orderbooks - - for x := range activeOrderbooks { - var ob gctrpc.Orderbooks - ob.Exchange = activeOrderbooks[x].ExchangeName - for y := range activeOrderbooks[x].ExchangeValues { - o := activeOrderbooks[x].ExchangeValues[y] - var bids []*gctrpc.OrderbookItem - for z := range o.Bids { - bids = append(bids, &gctrpc.OrderbookItem{ - Amount: o.Bids[z].Amount, - Price: o.Bids[z].Price, - }) - } - - var asks []*gctrpc.OrderbookItem - for z := range o.Asks { - asks = append(asks, &gctrpc.OrderbookItem{ - Amount: o.Asks[z].Amount, - Price: o.Asks[z].Price, - }) - } - - ob.Orderbooks = append(ob.Orderbooks, &gctrpc.OrderbookResponse{ - Pair: &gctrpc.CurrencyPair{ - Delimiter: o.Pair.Delimiter, - Base: o.Pair.Base.String(), - Quote: o.Pair.Quote.String(), - }, - LastUpdated: o.LastUpdated.Unix(), - Bids: bids, - Asks: asks, - }) +func (s *RPCServer) GetOrderbooks(_ context.Context, _ *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) { + exchanges := s.ExchangeManager.GetExchanges() + var obResponse []*gctrpc.Orderbooks + var obs []*gctrpc.OrderbookResponse + for x := range exchanges { + if !exchanges[x].IsEnabled() { + continue } - orderbooks = append(orderbooks, &ob) + assets := exchanges[x].GetAssetTypes() + exchName := exchanges[x].GetName() + for y := range assets { + currencies, err := exchanges[x].GetEnabledPairs(assets[y]) + if err != nil { + log.Errorf(log.RESTSys, + "Exchange %s could not retrieve enabled currencies. Err: %s\n", + exchName, + err) + continue + } + for z := range currencies { + resp, err := exchanges[x].FetchOrderbook(currencies[z], assets[y]) + if err != nil { + log.Errorf(log.RESTSys, + "Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName, + currencies[z].String(), + err) + continue + } + ob := &gctrpc.OrderbookResponse{ + Pair: &gctrpc.CurrencyPair{ + Delimiter: currencies[z].Delimiter, + Base: currencies[z].Base.String(), + Quote: currencies[z].Quote.String(), + }, + AssetType: assets[y].String(), + LastUpdated: resp.LastUpdated.Unix(), + } + for i := range resp.Bids { + ob.Bids = append(ob.Bids, &gctrpc.OrderbookItem{ + Amount: resp.Bids[i].Amount, + Price: resp.Bids[i].Price, + }) + } + + for i := range resp.Asks { + ob.Asks = append(ob.Asks, &gctrpc.OrderbookItem{ + Amount: resp.Asks[i].Amount, + Price: resp.Asks[i].Price, + }) + } + obs = append(obs, ob) + } + } + obResponse = append(obResponse, &gctrpc.Orderbooks{ + Exchange: exchanges[x].GetName(), + Orderbooks: obs, + }) } - return &gctrpc.GetOrderbooksResponse{Orderbooks: orderbooks}, nil + return &gctrpc.GetOrderbooksResponse{Orderbooks: obResponse}, nil } // GetAccountInfo returns an account balance for a specific exchange @@ -528,7 +549,7 @@ func (s *RPCServer) GetAccountInfo(_ context.Context, r *gctrpc.GetAccountInfoRe } // UpdateAccountInfo forces an update of the account info -func (s *RPCServer) UpdateAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { +func (s *RPCServer) UpdateAccountInfo(_ context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { assetType, err := asset.New(r.AssetType) if err != nil { return nil, err @@ -613,7 +634,12 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream return err } - defer pipe.Release() + defer func() { + pipeErr := pipe.Release() + if pipeErr != nil { + log.Error(log.DispatchMgr, pipeErr) + } + }() for { data, ok := <-pipe.C @@ -650,15 +676,14 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream } // GetConfig returns the bots config -func (s *RPCServer) GetConfig(_ context.Context, r *gctrpc.GetConfigRequest) (*gctrpc.GetConfigResponse, error) { +func (s *RPCServer) GetConfig(_ context.Context, _ *gctrpc.GetConfigRequest) (*gctrpc.GetConfigResponse, error) { return &gctrpc.GetConfigResponse{}, common.ErrNotYetImplemented } -// GetPortfolio returns the portfolio details -func (s *RPCServer) GetPortfolio(_ context.Context, r *gctrpc.GetPortfolioRequest) (*gctrpc.GetPortfolioResponse, error) { +// GetPortfolio returns the portfoliomanager details +func (s *RPCServer) GetPortfolio(_ context.Context, _ *gctrpc.GetPortfolioRequest) (*gctrpc.GetPortfolioResponse, error) { var addrs []*gctrpc.PortfolioAddress - botAddrs := s.Portfolio.Addresses - + botAddrs := s.portfolioManager.GetAddresses() for x := range botAddrs { addrs = append(addrs, &gctrpc.PortfolioAddress{ Address: botAddrs[x].Address, @@ -675,9 +700,9 @@ func (s *RPCServer) GetPortfolio(_ context.Context, r *gctrpc.GetPortfolioReques return resp, nil } -// GetPortfolioSummary returns the portfolio summary -func (s *RPCServer) GetPortfolioSummary(_ context.Context, r *gctrpc.GetPortfolioSummaryRequest) (*gctrpc.GetPortfolioSummaryResponse, error) { - result := s.Portfolio.GetPortfolioSummary() +// GetPortfolioSummary returns the portfoliomanager summary +func (s *RPCServer) GetPortfolioSummary(_ context.Context, _ *gctrpc.GetPortfolioSummaryRequest) (*gctrpc.GetPortfolioSummaryResponse, error) { + result := s.portfolioManager.GetPortfolioSummary() var resp gctrpc.GetPortfolioSummaryResponse p := func(coins []portfolio.Coin) []*gctrpc.Coin { @@ -731,9 +756,9 @@ func (s *RPCServer) GetPortfolioSummary(_ context.Context, r *gctrpc.GetPortfoli return &resp, nil } -// AddPortfolioAddress adds an address to the portfolio manager +// AddPortfolioAddress adds an address to the portfoliomanager manager func (s *RPCServer) AddPortfolioAddress(_ context.Context, r *gctrpc.AddPortfolioAddressRequest) (*gctrpc.GenericResponse, error) { - err := s.Portfolio.AddAddress(r.Address, + err := s.portfolioManager.AddAddress(r.Address, r.Description, currency.NewCode(r.CoinType), r.Balance) @@ -743,9 +768,9 @@ func (s *RPCServer) AddPortfolioAddress(_ context.Context, r *gctrpc.AddPortfoli return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } -// RemovePortfolioAddress removes an address from the portfolio manager +// RemovePortfolioAddress removes an address from the portfoliomanager manager func (s *RPCServer) RemovePortfolioAddress(_ context.Context, r *gctrpc.RemovePortfolioAddressRequest) (*gctrpc.GenericResponse, error) { - err := s.Portfolio.RemoveAddress(r.Address, + err := s.portfolioManager.RemoveAddress(r.Address, r.Description, currency.NewCode(r.CoinType)) if err != nil { @@ -755,7 +780,7 @@ func (s *RPCServer) RemovePortfolioAddress(_ context.Context, r *gctrpc.RemovePo } // GetForexProviders returns a list of available forex providers -func (s *RPCServer) GetForexProviders(_ context.Context, r *gctrpc.GetForexProvidersRequest) (*gctrpc.GetForexProvidersResponse, error) { +func (s *RPCServer) GetForexProviders(_ context.Context, _ *gctrpc.GetForexProvidersRequest) (*gctrpc.GetForexProvidersResponse, error) { providers := s.Config.GetForexProviders() if len(providers) == 0 { return nil, fmt.Errorf("forex providers is empty") @@ -777,7 +802,7 @@ func (s *RPCServer) GetForexProviders(_ context.Context, r *gctrpc.GetForexProvi } // GetForexRates returns a list of forex rates -func (s *RPCServer) GetForexRates(_ context.Context, r *gctrpc.GetForexRatesRequest) (*gctrpc.GetForexRatesResponse, error) { +func (s *RPCServer) GetForexRates(_ context.Context, _ *gctrpc.GetForexRatesRequest) (*gctrpc.GetForexRatesResponse, error) { rates, err := currency.GetExchangeRates() if err != nil { return nil, err @@ -1109,7 +1134,7 @@ func (s *RPCServer) WhaleBomb(_ context.Context, r *gctrpc.WhaleBombRequest) (*g } exch := s.GetExchangeByName(r.Exchange) - err := checkParams(r.Exchange, exch, asset.Item(""), p) + err := checkParams(r.Exchange, exch, asset.Spot, p) if err != nil { return nil, err } @@ -1126,6 +1151,9 @@ func (s *RPCServer) WhaleBomb(_ context.Context, r *gctrpc.WhaleBombRequest) (*g } result, err := o.WhaleBomb(r.PriceTarget, buy) + if err != nil { + return nil, err + } var resp gctrpc.SimulateOrderResponse for x := range result.Orders { resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{ @@ -1245,18 +1273,18 @@ func (s *RPCServer) CancelAllOrders(_ context.Context, r *gctrpc.CancelAllOrders } // GetEvents returns the stored events list -func (s *RPCServer) GetEvents(_ context.Context, r *gctrpc.GetEventsRequest) (*gctrpc.GetEventsResponse, error) { +func (s *RPCServer) GetEvents(_ context.Context, _ *gctrpc.GetEventsRequest) (*gctrpc.GetEventsResponse, error) { return &gctrpc.GetEventsResponse{}, common.ErrNotYetImplemented } // AddEvent adds an event func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gctrpc.AddEventResponse, error) { evtCondition := EventConditionParams{ - CheckBids: r.ConditionParams.CheckBids, - CheckBidsAndAsks: r.ConditionParams.CheckBidsAndAsks, - Condition: r.ConditionParams.Condition, - OrderbookAmount: r.ConditionParams.OrderbookAmount, - Price: r.ConditionParams.Price, + CheckBids: r.ConditionParams.CheckBids, + CheckAsks: r.ConditionParams.CheckAsks, + Condition: r.ConditionParams.Condition, + OrderbookAmount: r.ConditionParams.OrderbookAmount, + Price: r.ConditionParams.Price, } p := currency.NewPairWithDelimiter(r.Pair.Base, @@ -1273,7 +1301,7 @@ func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gct return nil, err } - id, err := Add(r.Exchange, r.Item, evtCondition, p, a, r.Action) + id, err := s.eventManager.Add(r.Exchange, r.Item, evtCondition, p, a, r.Action) if err != nil { return nil, err } @@ -1283,7 +1311,7 @@ func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gct // RemoveEvent removes an event, specified by an event ID func (s *RPCServer) RemoveEvent(_ context.Context, r *gctrpc.RemoveEventRequest) (*gctrpc.GenericResponse, error) { - if !Remove(r.Id) { + if !s.eventManager.Remove(r.Id) { return nil, fmt.Errorf("event %d not removed", r.Id) } return &gctrpc.GenericResponse{Status: MsgStatusSuccess, @@ -1335,7 +1363,7 @@ func (s *RPCServer) WithdrawCryptocurrencyFunds(_ context.Context, r *gctrpc.Wit }, } - resp, err := s.Engine.SubmitWithdrawal(request) + resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(request) if err != nil { return nil, err } @@ -1377,7 +1405,7 @@ func (s *RPCServer) WithdrawFiatFunds(_ context.Context, r *gctrpc.WithdrawFiatR }, } - resp, err := s.Engine.SubmitWithdrawal(request) + resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(request) if err != nil { return nil, err } @@ -1393,7 +1421,7 @@ func (s *RPCServer) WithdrawalEventByID(_ context.Context, r *gctrpc.WithdrawalE if !s.Config.Database.Enabled { return nil, database.ErrDatabaseSupportDisabled } - v, err := WithdrawalEventByID(r.Id) + v, err := s.WithdrawManager.WithdrawalEventByID(r.Id) if err != nil { return nil, err } @@ -1468,14 +1496,14 @@ func (s *RPCServer) WithdrawalEventsByExchange(_ context.Context, r *gctrpc.With return nil, database.ErrDatabaseSupportDisabled } if r.Id == "" { - ret, err := WithdrawalEventByExchange(r.Exchange, int(r.Limit)) + ret, err := s.WithdrawManager.WithdrawalEventByExchange(r.Exchange, int(r.Limit)) if err != nil { return nil, err } return parseMultipleEvents(ret), nil } - ret, err := WithdrawalEventByExchangeID(r.Exchange, r.Id) + ret, err := s.WithdrawManager.WithdrawalEventByExchangeID(r.Exchange, r.Id) if err != nil { return nil, err } @@ -1495,7 +1523,7 @@ func (s *RPCServer) WithdrawalEventsByDate(_ context.Context, r *gctrpc.Withdraw return nil, err } var ret []*withdraw.Response - ret, err = WithdrawEventByDate(r.Exchange, UTCStartTime, UTCEndTime, int(r.Limit)) + ret, err = s.WithdrawManager.WithdrawEventByDate(r.Exchange, UTCStartTime, UTCEndTime, int(r.Limit)) if err != nil { return nil, err } @@ -1715,7 +1743,12 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr return err } - defer pipe.Release() + defer func() { + pipeErr := pipe.Release() + if pipeErr != nil { + log.Error(log.DispatchMgr, pipeErr) + } + }() for { data, ok := <-pipe.C @@ -1780,7 +1813,12 @@ func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gct return err } - defer pipe.Release() + defer func() { + pipeErr := pipe.Release() + if pipeErr != nil { + log.Error(log.DispatchMgr, pipeErr) + } + }() for { data, ok := <-pipe.C @@ -1820,7 +1858,12 @@ func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamReq return err } - defer pipe.Release() + defer func() { + pipeErr := pipe.Release() + if pipeErr != nil { + log.Error(log.DispatchMgr, pipeErr) + } + }() for { data, ok := <-pipe.C @@ -2061,8 +2104,8 @@ func fillMissingCandlesWithStoredTrades(startTime, endTime time.Time, klineItem } // GCTScriptStatus returns a slice of current running scripts that includes next run time and uuid -func (s *RPCServer) GCTScriptStatus(_ context.Context, r *gctrpc.GCTScriptStatusRequest) (*gctrpc.GCTScriptStatusResponse, error) { - if !s.GctScriptManager.Started() { +func (s *RPCServer) GCTScriptStatus(_ context.Context, _ *gctrpc.GCTScriptStatusRequest) (*gctrpc.GCTScriptStatusResponse, error) { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptStatusResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2071,7 +2114,7 @@ func (s *RPCServer) GCTScriptStatus(_ context.Context, r *gctrpc.GCTScriptStatus } resp := &gctrpc.GCTScriptStatusResponse{ - Status: fmt.Sprintf("%v of %v virtual machines running", gctscript.VMSCount.Len(), s.GctScriptManager.GetMaxVirtualMachines()), + Status: fmt.Sprintf("%v of %v virtual machines running", gctscript.VMSCount.Len(), s.gctScriptManager.GetMaxVirtualMachines()), } gctscript.AllVMSync.Range(func(k, v interface{}) bool { @@ -2090,7 +2133,7 @@ func (s *RPCServer) GCTScriptStatus(_ context.Context, r *gctrpc.GCTScriptStatus // GCTScriptQuery queries a running script and returns script running information func (s *RPCServer) GCTScriptQuery(_ context.Context, r *gctrpc.GCTScriptQueryRequest) (*gctrpc.GCTScriptQueryResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptQueryResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2121,7 +2164,7 @@ func (s *RPCServer) GCTScriptQuery(_ context.Context, r *gctrpc.GCTScriptQueryRe // GCTScriptExecute execute a script func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecuteRequest) (*gctrpc.GenericResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2129,7 +2172,7 @@ func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecu r.Script.Path = gctscript.ScriptPath } - gctVM := s.GctScriptManager.New() + gctVM := s.gctScriptManager.New() if gctVM == nil { return &gctrpc.GenericResponse{Status: MsgStatusError, Data: "unable to create VM instance"}, nil } @@ -2153,7 +2196,7 @@ func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecu // GCTScriptStop terminate a running script func (s *RPCServer) GCTScriptStop(_ context.Context, r *gctrpc.GCTScriptStopRequest) (*gctrpc.GenericResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2175,7 +2218,7 @@ func (s *RPCServer) GCTScriptStop(_ context.Context, r *gctrpc.GCTScriptStopRequ // GCTScriptUpload upload a new script to ScriptPath func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUploadRequest) (*gctrpc.GenericResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2231,7 +2274,7 @@ func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUpload } var failedFiles []string for x := range files { - err = s.GctScriptManager.Validate(files[x]) + err = s.gctScriptManager.Validate(files[x]) if err != nil { failedFiles = append(failedFiles, files[x]) } @@ -2248,7 +2291,7 @@ func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUpload return &gctrpc.GenericResponse{Status: gctscript.ErrScriptFailedValidation, Data: strings.Join(failedFiles, ", ")}, nil } } else { - err = s.GctScriptManager.Validate(fPath) + err = s.gctScriptManager.Validate(fPath) if err != nil { errRemove := os.Remove(fPath) if errRemove != nil { @@ -2266,7 +2309,7 @@ func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUpload // GCTScriptReadScript read a script and return contents func (s *RPCServer) GCTScriptReadScript(_ context.Context, r *gctrpc.GCTScriptReadScriptRequest) (*gctrpc.GCTScriptQueryResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptQueryResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2291,7 +2334,7 @@ func (s *RPCServer) GCTScriptReadScript(_ context.Context, r *gctrpc.GCTScriptRe // GCTScriptListAll lists all scripts inside the default script path func (s *RPCServer) GCTScriptListAll(context.Context, *gctrpc.GCTScriptListAllRequest) (*gctrpc.GCTScriptStatusResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptStatusResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } @@ -2317,11 +2360,11 @@ func (s *RPCServer) GCTScriptListAll(context.Context, *gctrpc.GCTScriptListAllRe // GCTScriptStopAll stops all running scripts func (s *RPCServer) GCTScriptStopAll(context.Context, *gctrpc.GCTScriptStopAllRequest) (*gctrpc.GenericResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } - err := s.GctScriptManager.ShutdownAll() + err := s.gctScriptManager.ShutdownAll() if err != nil { return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil } @@ -2334,19 +2377,19 @@ func (s *RPCServer) GCTScriptStopAll(context.Context, *gctrpc.GCTScriptStopAllRe // GCTScriptAutoLoadToggle adds or removes an entry to the autoload list func (s *RPCServer) GCTScriptAutoLoadToggle(_ context.Context, r *gctrpc.GCTScriptAutoLoadRequest) (*gctrpc.GenericResponse, error) { - if !s.GctScriptManager.Started() { + if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } if r.Status { - err := s.GctScriptManager.Autoload(r.Script, true) + err := s.gctScriptManager.Autoload(r.Script, true) if err != nil { return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil } return &gctrpc.GenericResponse{Status: "success", Data: "script " + r.Script + " removed from autoload list"}, nil } - err := s.GctScriptManager.Autoload(r.Script, false) + err := s.gctScriptManager.Autoload(r.Script, false) if err != nil { return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil } @@ -2709,7 +2752,7 @@ func (s *RPCServer) ConvertTradesToCandles(_ context.Context, r *gctrpc.ConvertT return nil, err } if len(trades) == 0 { - return nil, fmt.Errorf("no trades returned from supplied params") + return nil, errNoTrades } interval := kline.Interval(r.TimeInterval) var klineItem kline.Item @@ -3010,11 +3053,12 @@ func (s *RPCServer) GetHistoricTrades(r *gctrpc.GetSavedTradesRequest, stream gc }) } - stream.Send(grpcTrades) + err = stream.Send(grpcTrades) + if err != nil { + return err + } } - stream.Send(resp) - - return nil + return stream.Send(resp) } // GetRecentTrades returns trades @@ -3069,7 +3113,7 @@ func checkParams(exchName string, e exchange.IBotExchange, a asset.Item, p curre return fmt.Errorf("%s %w", exchName, errExchangeNotLoaded) } if !e.IsEnabled() { - return fmt.Errorf("%s %w", exchName, errExchangeDisabled) + return fmt.Errorf("%s %w", exchName, ErrExchangeNotFound) } if a.IsValid() { b := e.GetBase() @@ -3100,3 +3144,141 @@ func checkParams(exchName string, e exchange.IBotExchange, a asset.Item, p curre } return fmt.Errorf("%v %w", p, errCurrencyPairInvalid) } + +func parseMultipleEvents(ret []*withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse { + v := &gctrpc.WithdrawalEventsByExchangeResponse{} + for x := range ret { + tempEvent := &gctrpc.WithdrawalEventResponse{ + Id: ret[x].ID.String(), + Exchange: &gctrpc.WithdrawlExchangeEvent{ + Name: ret[x].Exchange.Name, + Id: ret[x].Exchange.ID, + Status: ret[x].Exchange.Status, + }, + Request: &gctrpc.WithdrawalRequestEvent{ + Currency: ret[x].RequestDetails.Currency.String(), + Description: ret[x].RequestDetails.Description, + Amount: ret[x].RequestDetails.Amount, + Type: int32(ret[x].RequestDetails.Type), + }, + } + + tempEvent.CreatedAt = timestamppb.New(ret[x].CreatedAt) + if err := tempEvent.CreatedAt.CheckValid(); err != nil { + log.Errorf(log.Global, "withdrawal parseMultipleEvents CreatedAt: %s", err) + } + tempEvent.UpdatedAt = timestamppb.New(ret[x].UpdatedAt) + if err := tempEvent.UpdatedAt.CheckValid(); err != nil { + log.Errorf(log.Global, "withdrawal parseMultipleEvents UpdatedAt: %s", err) + } + + if ret[x].RequestDetails.Type == withdraw.Crypto { + tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) + tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ + Address: ret[x].RequestDetails.Crypto.Address, + AddressTag: ret[x].RequestDetails.Crypto.AddressTag, + Fee: ret[x].RequestDetails.Crypto.FeeAmount, + } + } else if ret[x].RequestDetails.Type == withdraw.Fiat { + if ret[x].RequestDetails.Fiat != (withdraw.FiatRequest{}) { + tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) + tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ + BankName: ret[x].RequestDetails.Fiat.Bank.BankName, + AccountName: ret[x].RequestDetails.Fiat.Bank.AccountName, + AccountNumber: ret[x].RequestDetails.Fiat.Bank.AccountNumber, + Bsb: ret[x].RequestDetails.Fiat.Bank.BSBNumber, + Swift: ret[x].RequestDetails.Fiat.Bank.SWIFTCode, + Iban: ret[x].RequestDetails.Fiat.Bank.IBAN, + } + } + } + v.Event = append(v.Event, tempEvent) + } + return v +} + +func parseWithdrawalsHistory(ret []exchange.WithdrawalHistory, exchName string, limit int) *gctrpc.WithdrawalEventsByExchangeResponse { + v := &gctrpc.WithdrawalEventsByExchangeResponse{} + for x := range ret { + if limit > 0 && x >= limit { + return v + } + + tempEvent := &gctrpc.WithdrawalEventResponse{ + Id: ret[x].TransferID, + Exchange: &gctrpc.WithdrawlExchangeEvent{ + Name: exchName, + Status: ret[x].Status, + }, + Request: &gctrpc.WithdrawalRequestEvent{ + Currency: ret[x].Currency, + Description: ret[x].Description, + Amount: ret[x].Amount, + }, + } + + tempEvent.UpdatedAt = timestamppb.New(ret[x].Timestamp) + if err := tempEvent.UpdatedAt.CheckValid(); err != nil { + log.Errorf(log.Global, "withdrawal parseWithdrawalsHistory UpdatedAt: %s", err) + } + + tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ + Address: ret[x].CryptoToAddress, + Fee: ret[x].Fee, + TxId: ret[x].CryptoTxID, + } + + v.Event = append(v.Event, tempEvent) + } + return v +} + +func parseSingleEvents(ret *withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse { + tempEvent := &gctrpc.WithdrawalEventResponse{ + Id: ret.ID.String(), + Exchange: &gctrpc.WithdrawlExchangeEvent{ + Name: ret.Exchange.Name, + Id: ret.Exchange.Name, + Status: ret.Exchange.Status, + }, + Request: &gctrpc.WithdrawalRequestEvent{ + Currency: ret.RequestDetails.Currency.String(), + Description: ret.RequestDetails.Description, + Amount: ret.RequestDetails.Amount, + Type: int32(ret.RequestDetails.Type), + }, + } + tempEvent.CreatedAt = timestamppb.New(ret.CreatedAt) + if err := tempEvent.CreatedAt.CheckValid(); err != nil { + log.Errorf(log.Global, "withdrawal parseSingleEvents CreatedAt %s", err) + } + tempEvent.UpdatedAt = timestamppb.New(ret.UpdatedAt) + if err := tempEvent.UpdatedAt.CheckValid(); err != nil { + log.Errorf(log.Global, "withdrawal parseSingleEvents UpdatedAt: %s", err) + } + + if ret.RequestDetails.Type == withdraw.Crypto { + tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) + tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ + Address: ret.RequestDetails.Crypto.Address, + AddressTag: ret.RequestDetails.Crypto.AddressTag, + Fee: ret.RequestDetails.Crypto.FeeAmount, + } + } else if ret.RequestDetails.Type == withdraw.Fiat { + if ret.RequestDetails.Fiat != (withdraw.FiatRequest{}) { + tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) + tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ + BankName: ret.RequestDetails.Fiat.Bank.BankName, + AccountName: ret.RequestDetails.Fiat.Bank.AccountName, + AccountNumber: ret.RequestDetails.Fiat.Bank.AccountNumber, + Bsb: ret.RequestDetails.Fiat.Bank.BSBNumber, + Swift: ret.RequestDetails.Fiat.Bank.SWIFTCode, + Iban: ret.RequestDetails.Fiat.Bank.IBAN, + } + } + } + + return &gctrpc.WithdrawalEventsByExchangeResponse{ + Event: []*gctrpc.WithdrawalEventResponse{tempEvent}, + } +} diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index af84f00a..9b21db05 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -3,11 +3,12 @@ package engine import ( "context" "errors" + "fmt" "log" "os" "path/filepath" + "reflect" "runtime" - "strings" "testing" "time" @@ -21,12 +22,15 @@ import ( dbexchange "github.com/thrasher-corp/gocryptotrader/database/repository/exchange" sqltrade "github.com/thrasher-corp/gocryptotrader/database/repository/trade" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/binance" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/gctrpc" + "github.com/thrasher-corp/gocryptotrader/portfolio/banking" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" "github.com/thrasher-corp/goose" ) @@ -37,14 +41,53 @@ const ( databaseName = "rpctestdb" ) +// fExchange is a fake exchange with function overrides +// we're not testing an actual exchange's implemented functions +type fExchange struct { + exchange.IBotExchange +} + +// FetchAccountInfo overrides testExchange's fetch account info function +// to do the bare minimum required with no API calls or credentials required +func (f fExchange) FetchAccountInfo(a asset.Item) (account.Holdings, error) { + return account.Holdings{ + Exchange: f.GetName(), + Accounts: []account.SubAccount{ + { + ID: "1337", + AssetType: a, + Currencies: nil, + }, + }, + }, nil +} + +// UpdateAccountInfo overrides testExchange's update account info function +// to do the bare minimum required with no API calls or credentials required +func (f fExchange) UpdateAccountInfo(a asset.Item) (account.Holdings, error) { + if a == asset.Futures { + return account.Holdings{}, errAssetTypeDisabled + } + return account.Holdings{ + Exchange: f.GetName(), + Accounts: []account.SubAccount{ + { + ID: "1337", + AssetType: a, + Currencies: nil, + }, + }, + }, nil +} + // Sets up everything required to run any function inside rpcserver func RPCTestSetup(t *testing.T) *Engine { - database.DB.Mu.Lock() var err error dbConf := database.Config{ Enabled: true, Driver: database.DBSQLite3, ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", Database: databaseName, }, } @@ -54,20 +97,22 @@ func RPCTestSetup(t *testing.T) *Engine { if err != nil { t.Fatalf("SetupTest: Failed to load config: %s", err) } - - if engerino.GetExchangeByName(testExchange) == nil { - err = engerino.LoadExchange(testExchange, false, nil) - if err != nil { - t.Fatalf("SetupTest: Failed to load exchange: %s", err) - } + engerino.ExchangeManager = SetupExchangeManager() + err = engerino.LoadExchange(testExchange, false, nil) + if err != nil { + log.Fatal(err) } engerino.Config.Database = dbConf - err = engerino.DatabaseManager.Start(engerino) + engerino.DatabaseManager, err = SetupDatabaseConnectionManager(&engerino.Config.Database) + if err != nil { + log.Fatal(err) + } + err = engerino.DatabaseManager.Start(&engerino.ServicesWG) if err != nil { log.Fatal(err) } path := filepath.Join("..", databaseFolder, migrationsFolder) - err = goose.Run("up", dbConn.SQL, repository.GetSQLDialect(), path, "") + err = goose.Run("up", database.DB.SQL, repository.GetSQLDialect(), path, "") if err != nil { t.Fatalf("failed to run migrations %v", err) } @@ -76,14 +121,11 @@ func RPCTestSetup(t *testing.T) *Engine { if err != nil { t.Fatalf("failed to insert exchange %v", err) } - database.DB.Mu.Unlock() return engerino } func CleanRPCTest(t *testing.T, engerino *Engine) { - database.DB.Mu.Lock() - defer database.DB.Mu.Unlock() err := engerino.DatabaseManager.Stop() if err != nil { t.Error(err) @@ -100,9 +142,6 @@ func TestGetSavedTrades(t *testing.T) { defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} _, err := s.GetSavedTrades(context.Background(), &gctrpc.GetSavedTradesRequest{}) - if err == nil { - t.Fatal(unexpectedLackOfError) - } if !errors.Is(err, errInvalidArguments) { t.Error(err) } @@ -117,10 +156,6 @@ func TestGetSavedTrades(t *testing.T) { Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat), }) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errExchangeNotLoaded) { t.Error(err) } @@ -178,10 +213,6 @@ func TestConvertTradesToCandles(t *testing.T) { s := RPCServer{Engine: engerino} // bad param test _, err := s.ConvertTradesToCandles(context.Background(), &gctrpc.ConvertTradesToCandlesRequest{}) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errInvalidArguments) { t.Error(err) } @@ -199,10 +230,6 @@ func TestConvertTradesToCandles(t *testing.T) { End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat), TimeInterval: int64(kline.OneHour.Duration()), }) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errExchangeNotLoaded) { t.Error(err) } @@ -220,17 +247,13 @@ func TestConvertTradesToCandles(t *testing.T) { End: time.Date(2020, 2, 2, 2, 2, 2, 2, time.UTC).Format(common.SimpleTimeFormat), TimeInterval: int64(kline.OneHour.Duration()), }) - if err == nil { - t.Error(unexpectedLackOfError) - return - } - if err.Error() != "no trades returned from supplied params" { - t.Error(err) + if !errors.Is(err, errNoTrades) { + t.Errorf("received '%v' expected '%v'", err, errNoTrades) } // add a trade err = sqltrade.Insert(sqltrade.Data{ - Timestamp: time.Date(2020, 1, 1, 1, 1, 2, 1, time.UTC), + Timestamp: time.Date(2020, 1, 1, 1, 2, 2, 1, time.UTC), Exchange: testExchange, Base: currency.BTC.String(), Quote: currency.USD.String(), @@ -240,8 +263,7 @@ func TestConvertTradesToCandles(t *testing.T) { Side: order.Buy.String(), }) if err != nil { - t.Error(err) - return + t.Fatal(err) } // get candle from one trade @@ -255,7 +277,7 @@ func TestConvertTradesToCandles(t *testing.T) { }, AssetType: asset.Spot.String(), Start: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat), - End: time.Date(2020, 2, 2, 2, 2, 2, 2, time.UTC).Format(common.SimpleTimeFormat), + End: time.Date(2020, 3, 2, 2, 2, 2, 2, time.UTC).Format(common.SimpleTimeFormat), TimeInterval: int64(kline.OneHour.Duration()), }) if err != nil { @@ -712,10 +734,6 @@ func TestGetRecentTrades(t *testing.T) { defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} _, err := s.GetRecentTrades(context.Background(), &gctrpc.GetSavedTradesRequest{}) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errInvalidArguments) { t.Error(err) } @@ -730,10 +748,6 @@ func TestGetRecentTrades(t *testing.T) { Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat), }) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errExchangeNotLoaded) { t.Error(err) } @@ -756,10 +770,6 @@ func TestGetHistoricTrades(t *testing.T) { defer CleanRPCTest(t, engerino) s := RPCServer{Engine: engerino} err := s.GetHistoricTrades(&gctrpc.GetSavedTradesRequest{}, nil) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errInvalidArguments) { t.Error(err) } @@ -774,10 +784,6 @@ func TestGetHistoricTrades(t *testing.T) { Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat), }, nil) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if !errors.Is(err, errExchangeNotLoaded) { t.Error(err) } @@ -792,10 +798,6 @@ func TestGetHistoricTrades(t *testing.T) { Start: time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC).Format(common.SimpleTimeFormat), End: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC).Format(common.SimpleTimeFormat), }, nil) - if err == nil { - t.Error(unexpectedLackOfError) - return - } if err != common.ErrFunctionNotSupported { t.Error(err) } @@ -803,39 +805,48 @@ func TestGetHistoricTrades(t *testing.T) { func TestGetAccountInfo(t *testing.T) { bot := CreateTestBot(t) + exch := bot.ExchangeManager.GetExchangeByName(testExchange) + b := exch.GetBase() + b.Name = "fake" + fakeExchange := fExchange{ + IBotExchange: exch, + } + bot.ExchangeManager.Add(fakeExchange) s := RPCServer{Engine: bot} - r, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakePassExchange, AssetType: asset.Spot.String()}) - if err != nil { - t.Fatalf("TestGetAccountInfo: Failed to get account info: %s", err) - } - if r.Accounts[0].Currencies[0].TotalValue != 10 { - t.Fatal("TestGetAccountInfo: Unexpected value of the 'TotalValue'") + _, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Spot.String()}) + if !errors.Is(err, nil) { + t.Errorf("expected %v, received %v", errAssetTypeDisabled, nil) } } func TestUpdateAccountInfo(t *testing.T) { bot := CreateTestBot(t) + exch := bot.ExchangeManager.GetExchangeByName(testExchange) + b := exch.GetBase() + b.Name = "fake" + fakeExchange := fExchange{ + IBotExchange: exch, + } + bot.ExchangeManager.Add(fakeExchange) s := RPCServer{Engine: bot} - getResponse, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakePassExchange, AssetType: asset.Spot.String()}) - if err != nil { - t.Fatalf("TestGetAccountInfo: Failed to get account info: %s", err) + _, err := s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Spot.String()}) + if !errors.Is(err, nil) { + t.Errorf("expected %v, received %v", nil, err) } - _, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakePassExchange, AssetType: asset.Futures.String()}) + _, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Futures.String()}) if !errors.Is(err, errAssetTypeDisabled) { t.Errorf("expected %v, received %v", errAssetTypeDisabled, err) } - updateResp, err := s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{ - Exchange: fakePassExchange, + _, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{ + Exchange: "fake", AssetType: asset.Spot.String(), }) if !errors.Is(err, nil) { - t.Error(err) - } else if getResponse.Accounts[0].Currencies[0].TotalValue == updateResp.Accounts[0].Currencies[0].TotalValue { - t.Fatalf("TestGetAccountInfo: Unexpected value of the 'TotalValue'") + t.Errorf("expected %v, received %v", nil, err) } } @@ -903,11 +914,8 @@ func TestGetOrders(t *testing.T) { StartDate: time.Now().Format(common.SimpleTimeFormat), EndDate: time.Now().Add(time.Hour).Format(common.SimpleTimeFormat), }) - if err != nil && !strings.Contains(err.Error(), "not supported due to unset/default API keys") { - t.Error(err) - } - if err == nil { - t.Error("expected error") + if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) { + t.Errorf("received '%v', expected '%v'", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } exch := engerino.GetExchangeByName(exchName) @@ -982,18 +990,24 @@ func TestGetOrder(t *testing.T) { t.Errorf("expected %v, received %v", asset.ErrNotSupported, err) } + s.OrderManager, err = SetupOrderManager(engerino.ExchangeManager, engerino.CommunicationsManager, &engerino.ServicesWG, engerino.Settings.Verbose) + if err != nil { + t.Fatal(err) + } + + err = s.OrderManager.Start() + if err != nil { + t.Fatal(err) + } + _, err = s.GetOrder(context.Background(), &gctrpc.GetOrderRequest{ Exchange: exchName, OrderId: "", Pair: p, Asset: asset.Spot.String(), }) - if !errors.Is(err, errOrderIDCannotBeEmpty) { - t.Errorf("expected %v, received %v", errOrderIDCannotBeEmpty, err) - } - err = engerino.OrderManager.Start(engerino) - if err != nil { - t.Fatal(err) + if !errors.Is(err, ErrOrderIDCannotBeEmpty) { + t.Errorf("expected %v, received %v", ErrOrderIDCannotBeEmpty, err) } _, err = s.GetOrder(context.Background(), &gctrpc.GetOrderRequest{ Exchange: exchName, @@ -1001,8 +1015,8 @@ func TestGetOrder(t *testing.T) { Pair: p, Asset: asset.Spot.String(), }) - if err == nil { - t.Error("expected error") + if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) { + t.Errorf("expected '%v' received '%v'", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } } @@ -1021,8 +1035,8 @@ func TestCheckVars(t *testing.T) { } err = checkParams("Binance", e, asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) - if !errors.Is(err, errExchangeDisabled) { - t.Errorf("expected %v, got %v", errExchangeDisabled, err) + if !errors.Is(err, ErrExchangeNotFound) { + t.Errorf("expected %v, got %v", ErrExchangeNotFound, err) } e.SetEnabled(true) @@ -1090,13 +1104,77 @@ func TestCheckVars(t *testing.T) { t.Errorf("expected %v, got %v", errCurrencyNotEnabled, err) } - e.GetBase().CurrencyPairs.EnablePair( + err = e.GetBase().CurrencyPairs.EnablePair( asset.Spot, currency.Pair{Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.USDT}, ) + if err != nil { + t.Error(err) + } err = checkParams("Binance", e, asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) if err != nil { t.Error(err) } } + +func TestParseEvents(t *testing.T) { + var exchangeName = "Binance" + var testData []*withdraw.Response + for x := 0; x < 5; x++ { + test := fmt.Sprintf("test-%v", x) + resp := &withdraw.Response{ + ID: withdraw.DryRunID, + Exchange: withdraw.ExchangeResponse{ + Name: test, + ID: test, + Status: test, + }, + RequestDetails: withdraw.Request{ + Exchange: test, + Description: test, + Amount: 1.0, + }, + } + if x%2 == 0 { + resp.RequestDetails.Currency = currency.AUD + resp.RequestDetails.Type = 1 + resp.RequestDetails.Fiat = withdraw.FiatRequest{ + Bank: banking.Account{ + Enabled: false, + ID: fmt.Sprintf("test-%v", x), + BankName: fmt.Sprintf("test-%v-bank", x), + AccountName: "hello", + AccountNumber: fmt.Sprintf("test-%v", x), + BSBNumber: "123456", + SupportedCurrencies: "BTC-AUD", + SupportedExchanges: exchangeName, + }, + } + } else { + resp.RequestDetails.Currency = currency.BTC + resp.RequestDetails.Type = 0 + resp.RequestDetails.Crypto.Address = test + resp.RequestDetails.Crypto.FeeAmount = 0 + resp.RequestDetails.Crypto.AddressTag = test + } + testData = append(testData, resp) + } + v := parseMultipleEvents(testData) + if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" { + t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse") + } + if testData == nil || len(testData) < 2 { + t.Fatal("expected at least 2") + } + + v = parseSingleEvents(testData[0]) + if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" { + t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse") + } + + v = parseSingleEvents(testData[1]) + if v.Event[0].Request.Type != 0 { + t.Fatal("Expected second entry in slice to return a Request.Type of Crypto") + } +} diff --git a/engine/subsystem/subsystem.go b/engine/subsystem/subsystem.go deleted file mode 100644 index 7aeebadd..00000000 --- a/engine/subsystem/subsystem.go +++ /dev/null @@ -1,21 +0,0 @@ -package subsystem - -import "errors" - -const ( - // MsgSubSystemStarting message to return when subsystem is starting up - MsgSubSystemStarting = "manager starting..." - // MsgSubSystemStarted message to return when subsystem has started - MsgSubSystemStarted = "started." - // MsgSubSystemShuttingDown message to return when a subsystem is shutting down - MsgSubSystemShuttingDown = "shutting down..." - // MsgSubSystemShutdown message to return when a subsystem has shutdown - MsgSubSystemShutdown = "manager shutdown." -) - -var ( - // ErrSubSystemAlreadyStarted message to return when a subsystem is already started - ErrSubSystemAlreadyStarted = errors.New("manager already started") - // ErrSubSystemNotStarted message to return when subsystem not started - ErrSubSystemNotStarted = errors.New("not started") -) diff --git a/engine/subsystem_types.go b/engine/subsystem_types.go new file mode 100644 index 00000000..455c878c --- /dev/null +++ b/engine/subsystem_types.go @@ -0,0 +1,85 @@ +package engine + +import ( + "errors" + + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/portfolio" +) + +const ( + // MsgSubSystemStarting message to return when subsystem is starting up + MsgSubSystemStarting = "starting..." + // MsgSubSystemStarted message to return when subsystem has started + MsgSubSystemStarted = "started." + // MsgSubSystemShuttingDown message to return when a subsystem is shutting down + MsgSubSystemShuttingDown = "shutting down..." + // MsgSubSystemShutdown message to return when a subsystem has shutdown + MsgSubSystemShutdown = "shutdown." +) + +var ( + // ErrSubSystemAlreadyStarted message to return when a subsystem is already started + ErrSubSystemAlreadyStarted = errors.New("subsystem already started") + // ErrSubSystemNotStarted message to return when subsystem not started + ErrSubSystemNotStarted = errors.New("subsystem not started") + // ErrNilSubsystem is returned when a subsystem hasn't had its Setup() func run + ErrNilSubsystem = errors.New("subsystem not setup") + errNilWaitGroup = errors.New("nil wait group received") + errNilExchangeManager = errors.New("cannot start with nil exchange manager") +) + +// iExchangeManager limits exposure of accessible functions to exchange manager +// so that subsystems can use some functionality +type iExchangeManager interface { + GetExchanges() []exchange.IBotExchange + GetExchangeByName(string) exchange.IBotExchange +} + +// iCommsManager limits exposure of accessible functions to communication manager +type iCommsManager interface { + PushEvent(evt base.Event) +} + +// iOrderManager defines a limited scoped order manager +type iOrderManager interface { + Exists(*order.Detail) bool + Add(*order.Detail) error + Cancel(*order.Cancel) error + GetByExchangeAndID(string, string) (*order.Detail, error) + UpdateExistingOrder(*order.Detail) error +} + +// iPortfolioManager limits exposure of accessible functions to portfolio manager +type iPortfolioManager interface { + GetPortfolioSummary() portfolio.Summary + IsWhiteListed(string) bool + IsExchangeSupported(string, string) bool +} + +// iBot limits exposure of accessible functions to engine bot +type iBot interface { + SetupExchanges() error +} + +// iWebsocketDataReceiver limits exposure of accessible functions to websocket data receiver +type iWebsocketDataReceiver interface { + IsRunning() bool + WebsocketDataReceiver(ws *stream.Websocket) + WebsocketDataHandler(string, interface{}) error +} + +// iCurrencyPairSyncer defines a limited scoped currency pair syncer +type iCurrencyPairSyncer interface { + IsRunning() bool + PrintTickerSummary(*ticker.Price, string, error) + PrintOrderbookSummary(*orderbook.Base, string, error) + Update(string, currency.Pair, asset.Item, int, error) error +} diff --git a/engine/subsystem_types.md b/engine/subsystem_types.md new file mode 100644 index 00000000..a0590fa7 --- /dev/null +++ b/engine/subsystem_types.md @@ -0,0 +1,47 @@ +# GoCryptoTrader package Subsystem_types + + + + +[![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/subsystem_types) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This subsystem_types 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Subsystem_types ++ Subsystem contains subsystems that are used at run time by an `engine.Engine`, however they can be setup and run individually. ++ Subsystems are designed to be self contained ++ All subsystems have a public `Setup(...) (..., error)` function to return a valid subsystem ready for use + + Subsystems which are designed to be switched off also have `Start(...) error`, `IsRunning() bool` and `Stop(...) error` functions to allow the main `engine.Engine` instance to manage them ++ Common subsystem types such as errors can be found within the `subsystem.go` file + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/sync_manager.go b/engine/sync_manager.go new file mode 100644 index 00000000..2de652b6 --- /dev/null +++ b/engine/sync_manager.go @@ -0,0 +1,893 @@ +package engine + +import ( + "errors" + "fmt" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/stats" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// const holds the sync item types +const ( + SyncItemTicker = iota + SyncItemOrderbook + SyncItemTrade + SyncManagerName = "exchange_syncer" +) + +var ( + createdCounter = 0 + removedCounter = 0 + // DefaultSyncerWorkers limits the number of sync workers + DefaultSyncerWorkers = 15 + // DefaultSyncerTimeoutREST the default time to switch from REST to websocket protocols without a response + DefaultSyncerTimeoutREST = time.Second * 15 + DefaultSyncerTimeoutWebsocket = time.Minute + errNoSyncItemsEnabled = errors.New("no sync items enabled") + errUnknownSyncItem = errors.New("unknown sync item") +) + +// setupSyncManager starts a new CurrencyPairSyncer +func setupSyncManager(c *Config, exchangeManager iExchangeManager, websocketDataReceiver iWebsocketDataReceiver, remoteConfig *config.RemoteControlConfig) (*syncManager, error) { + if !c.SyncOrderbook && !c.SyncTicker && !c.SyncTrades { + return nil, errNoSyncItemsEnabled + } + if exchangeManager == nil { + return nil, errNilExchangeManager + } + if remoteConfig == nil { + return nil, errNilConfig + } + + if c.NumWorkers <= 0 { + c.NumWorkers = DefaultSyncerWorkers + } + + if c.SyncTimeoutREST <= time.Duration(0) { + c.SyncTimeoutREST = DefaultSyncerTimeoutREST + } + + if c.SyncTimeoutWebsocket <= time.Duration(0) { + c.SyncTimeoutWebsocket = DefaultSyncerTimeoutWebsocket + } + + s := &syncManager{ + config: *c, + remoteConfig: remoteConfig, + exchangeManager: exchangeManager, + websocketDataReceiver: websocketDataReceiver, + } + + s.tickerBatchLastRequested = make(map[string]time.Time) + + log.Debugf(log.SyncMgr, + "Exchange currency pair syncer config: continuous: %v ticker: %v"+ + " orderbook: %v trades: %v workers: %v verbose: %v timeout REST: %v"+ + " timeout Websocket: %v\n", + s.config.SyncContinuously, s.config.SyncTicker, s.config.SyncOrderbook, + s.config.SyncTrades, s.config.NumWorkers, s.config.Verbose, s.config.SyncTimeoutREST, + s.config.SyncTimeoutWebsocket) + return s, nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *syncManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Start runs the subsystem +func (m *syncManager) Start() error { + if m == nil { + return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return ErrSubSystemAlreadyStarted + } + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer started.") + exchanges := m.exchangeManager.GetExchanges() + for x := range exchanges { + exchangeName := exchanges[x].GetName() + supportsWebsocket := exchanges[x].SupportsWebsocket() + assetTypes := exchanges[x].GetAssetTypes() + supportsREST := exchanges[x].SupportsREST() + + if !supportsREST && !supportsWebsocket { + log.Warnf(log.SyncMgr, + "Loaded exchange %s does not support REST or Websocket.\n", + exchangeName) + continue + } + + var usingWebsocket bool + var usingREST bool + if supportsWebsocket && exchanges[x].IsWebsocketEnabled() { + ws, err := exchanges[x].GetWebsocket() + if err != nil { + log.Errorf(log.SyncMgr, + "%s failed to get websocket. Err: %s\n", + exchangeName, + err) + usingREST = true + } + + if !ws.IsConnected() && !ws.IsConnecting() { + if m.websocketDataReceiver.IsRunning() { + go m.websocketDataReceiver.WebsocketDataReceiver(ws) + } + + err = ws.Connect() + if err == nil { + err = ws.FlushChannels() + } + if err != nil { + log.Errorf(log.SyncMgr, + "%s websocket failed to connect. Err: %s\n", + exchangeName, + err) + usingREST = true + } else { + usingWebsocket = true + } + } else { + usingWebsocket = true + } + } else if supportsREST { + usingREST = true + } + + for y := range assetTypes { + if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil { + log.Warnf(log.SyncMgr, + "%s asset type %s is disabled, fetching enabled pairs is paused", + exchangeName, + assetTypes[y]) + continue + } + + wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y]) + if !wsAssetSupported { + log.Warnf(log.SyncMgr, + "%s asset type %s websocket functionality is unsupported, REST fetching only.", + exchangeName, + assetTypes[y]) + } + enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y]) + if err != nil { + log.Errorf(log.SyncMgr, + "%s failed to get enabled pairs. Err: %s\n", + exchangeName, + err) + continue + } + for i := range enabledPairs { + if m.exists(exchangeName, enabledPairs[i], assetTypes[y]) { + continue + } + + c := currencyPairSyncAgent{ + AssetType: assetTypes[y], + Exchange: exchangeName, + Pair: enabledPairs[i], + } + + sBase := syncBase{ + IsUsingREST: usingREST || !wsAssetSupported, + IsUsingWebsocket: usingWebsocket && wsAssetSupported, + } + + if m.config.SyncTicker { + c.Ticker = sBase + } + + if m.config.SyncOrderbook { + c.Orderbook = sBase + } + + if m.config.SyncTrades { + c.Trade = sBase + } + + m.add(&c) + } + } + } + + if atomic.CompareAndSwapInt32(&m.initSyncStarted, 0, 1) { + log.Debugf(log.SyncMgr, + "Exchange CurrencyPairSyncer initial sync started. %d items to process.\n", + createdCounter) + m.initSyncStartTime = time.Now() + } + + go func() { + m.initSyncWG.Wait() + if atomic.CompareAndSwapInt32(&m.initSyncCompleted, 0, 1) { + log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync is complete.\n") + completedTime := time.Now() + log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync took %v [%v sync items].\n", + completedTime.Sub(m.initSyncStartTime), createdCounter) + + if !m.config.SyncContinuously { + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopping.") + err := m.Stop() + if err != nil { + log.Error(log.SyncMgr, err) + } + return + } + } + }() + + if atomic.LoadInt32(&m.initSyncCompleted) == 1 && !m.config.SyncContinuously { + return nil + } + + for i := 0; i < m.config.NumWorkers; i++ { + go m.worker() + } + return nil +} + +// Stop shuts down the exchange currency pair syncer +// Stop attempts to shutdown the subsystem +func (m *syncManager) Stop() error { + if m == nil { + return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 1, 0) { + return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrSubSystemNotStarted) + } + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopped.") + return nil +} + +func (m *syncManager) get(exchangeName string, p currency.Pair, a asset.Item) (*currencyPairSyncAgent, error) { + m.mux.Lock() + defer m.mux.Unlock() + + for x := range m.currencyPairs { + if m.currencyPairs[x].Exchange == exchangeName && + m.currencyPairs[x].Pair.Equal(p) && + m.currencyPairs[x].AssetType == a { + return &m.currencyPairs[x], nil + } + } + + return nil, errors.New("exchange currency pair syncer not found") +} + +func (m *syncManager) exists(exchangeName string, p currency.Pair, a asset.Item) bool { + m.mux.Lock() + defer m.mux.Unlock() + + for x := range m.currencyPairs { + if m.currencyPairs[x].Exchange == exchangeName && + m.currencyPairs[x].Pair.Equal(p) && + m.currencyPairs[x].AssetType == a { + return true + } + } + return false +} + +func (m *syncManager) add(c *currencyPairSyncAgent) { + m.mux.Lock() + defer m.mux.Unlock() + + if m.config.SyncTicker { + if m.config.Verbose { + log.Debugf(log.SyncMgr, + "%s: Added ticker sync item %v: using websocket: %v using REST: %v\n", + c.Exchange, m.FormatCurrency(c.Pair).String(), c.Ticker.IsUsingWebsocket, + c.Ticker.IsUsingREST) + } + if atomic.LoadInt32(&m.initSyncCompleted) != 1 { + m.initSyncWG.Add(1) + createdCounter++ + } + } + + if m.config.SyncOrderbook { + if m.config.Verbose { + log.Debugf(log.SyncMgr, + "%s: Added orderbook sync item %v: using websocket: %v using REST: %v\n", + c.Exchange, m.FormatCurrency(c.Pair).String(), c.Orderbook.IsUsingWebsocket, + c.Orderbook.IsUsingREST) + } + if atomic.LoadInt32(&m.initSyncCompleted) != 1 { + m.initSyncWG.Add(1) + createdCounter++ + } + } + + if m.config.SyncTrades { + if m.config.Verbose { + log.Debugf(log.SyncMgr, + "%s: Added trade sync item %v: using websocket: %v using REST: %v\n", + c.Exchange, m.FormatCurrency(c.Pair).String(), c.Trade.IsUsingWebsocket, + c.Trade.IsUsingREST) + } + if atomic.LoadInt32(&m.initSyncCompleted) != 1 { + m.initSyncWG.Add(1) + createdCounter++ + } + } + + c.Created = time.Now() + m.currencyPairs = append(m.currencyPairs, *c) +} + +func (m *syncManager) isProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int) bool { + m.mux.Lock() + defer m.mux.Unlock() + + for x := range m.currencyPairs { + if m.currencyPairs[x].Exchange == exchangeName && + m.currencyPairs[x].Pair.Equal(p) && + m.currencyPairs[x].AssetType == a { + switch syncType { + case SyncItemTicker: + return m.currencyPairs[x].Ticker.IsProcessing + case SyncItemOrderbook: + return m.currencyPairs[x].Orderbook.IsProcessing + case SyncItemTrade: + return m.currencyPairs[x].Trade.IsProcessing + } + } + } + + return false +} + +func (m *syncManager) setProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int, processing bool) { + m.mux.Lock() + defer m.mux.Unlock() + + for x := range m.currencyPairs { + if m.currencyPairs[x].Exchange == exchangeName && + m.currencyPairs[x].Pair.Equal(p) && + m.currencyPairs[x].AssetType == a { + switch syncType { + case SyncItemTicker: + m.currencyPairs[x].Ticker.IsProcessing = processing + case SyncItemOrderbook: + m.currencyPairs[x].Orderbook.IsProcessing = processing + case SyncItemTrade: + m.currencyPairs[x].Trade.IsProcessing = processing + } + } + } +} + +// Update notifies the syncManager to change the last updated time for a exchange asset pair +func (m *syncManager) Update(exchangeName string, p currency.Pair, a asset.Item, syncType int, err error) error { + if m == nil { + return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) + } + if atomic.LoadInt32(&m.started) == 0 { + return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrSubSystemNotStarted) + } + + if atomic.LoadInt32(&m.initSyncStarted) != 1 { + return nil + } + + switch syncType { + case SyncItemOrderbook: + if !m.config.SyncOrderbook { + return nil + } + case SyncItemTicker: + if !m.config.SyncTicker { + return nil + } + case SyncItemTrade: + if !m.config.SyncTrades { + return nil + } + default: + return fmt.Errorf("%v %w", syncType, errUnknownSyncItem) + } + + m.mux.Lock() + defer m.mux.Unlock() + + for x := range m.currencyPairs { + if m.currencyPairs[x].Exchange == exchangeName && + m.currencyPairs[x].Pair.Equal(p) && + m.currencyPairs[x].AssetType == a { + switch syncType { + case SyncItemTicker: + origHadData := m.currencyPairs[x].Ticker.HaveData + m.currencyPairs[x].Ticker.LastUpdated = time.Now() + if err != nil { + m.currencyPairs[x].Ticker.NumErrors++ + } + m.currencyPairs[x].Ticker.HaveData = true + m.currencyPairs[x].Ticker.IsProcessing = false + if atomic.LoadInt32(&m.initSyncCompleted) != 1 && !origHadData { + removedCounter++ + log.Debugf(log.SyncMgr, "%s ticker sync complete %v [%d/%d].\n", + exchangeName, + m.FormatCurrency(p).String(), + removedCounter, + createdCounter) + m.initSyncWG.Done() + } + + case SyncItemOrderbook: + origHadData := m.currencyPairs[x].Orderbook.HaveData + m.currencyPairs[x].Orderbook.LastUpdated = time.Now() + if err != nil { + m.currencyPairs[x].Orderbook.NumErrors++ + } + m.currencyPairs[x].Orderbook.HaveData = true + m.currencyPairs[x].Orderbook.IsProcessing = false + if atomic.LoadInt32(&m.initSyncCompleted) != 1 && !origHadData { + removedCounter++ + log.Debugf(log.SyncMgr, "%s orderbook sync complete %v [%d/%d].\n", + exchangeName, + m.FormatCurrency(p).String(), + removedCounter, + createdCounter) + m.initSyncWG.Done() + } + + case SyncItemTrade: + origHadData := m.currencyPairs[x].Trade.HaveData + m.currencyPairs[x].Trade.LastUpdated = time.Now() + if err != nil { + m.currencyPairs[x].Trade.NumErrors++ + } + m.currencyPairs[x].Trade.HaveData = true + m.currencyPairs[x].Trade.IsProcessing = false + if atomic.LoadInt32(&m.initSyncCompleted) != 1 && !origHadData { + removedCounter++ + log.Debugf(log.SyncMgr, "%s trade sync complete %v [%d/%d].\n", + exchangeName, + m.FormatCurrency(p).String(), + removedCounter, + createdCounter) + m.initSyncWG.Done() + } + } + } + } + return nil +} + +func (m *syncManager) worker() { + cleanup := func() { + log.Debugln(log.SyncMgr, + "Exchange CurrencyPairSyncer worker shutting down.") + } + defer cleanup() + + for atomic.LoadInt32(&m.started) != 0 { + exchanges := m.exchangeManager.GetExchanges() + for x := range exchanges { + exchangeName := exchanges[x].GetName() + assetTypes := exchanges[x].GetAssetTypes() + supportsREST := exchanges[x].SupportsREST() + supportsRESTTickerBatching := exchanges[x].SupportsRESTTickerBatchUpdates() + var usingREST bool + var usingWebsocket bool + var switchedToRest bool + if exchanges[x].SupportsWebsocket() && exchanges[x].IsWebsocketEnabled() { + ws, err := exchanges[x].GetWebsocket() + if err != nil { + log.Errorf(log.SyncMgr, + "%s unable to get websocket pointer. Err: %s\n", + exchangeName, + err) + usingREST = true + } + + if ws.IsConnected() { + usingWebsocket = true + } else { + usingREST = true + } + } else if supportsREST { + usingREST = true + } + + for y := range assetTypes { + if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil { + continue + } + wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y]) + enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y]) + if err != nil { + log.Errorf(log.SyncMgr, + "%s failed to get enabled pairs. Err: %s\n", + exchangeName, + err) + continue + } + for i := range enabledPairs { + if atomic.LoadInt32(&m.started) == 0 { + return + } + + if !m.exists(exchangeName, enabledPairs[i], assetTypes[y]) { + c := currencyPairSyncAgent{ + AssetType: assetTypes[y], + Exchange: exchangeName, + Pair: enabledPairs[i], + } + + sBase := syncBase{ + IsUsingREST: usingREST || !wsAssetSupported, + IsUsingWebsocket: usingWebsocket && wsAssetSupported, + } + + if m.config.SyncTicker { + c.Ticker = sBase + } + + if m.config.SyncOrderbook { + c.Orderbook = sBase + } + + if m.config.SyncTrades { + c.Trade = sBase + } + + m.add(&c) + } + + c, err := m.get(exchangeName, enabledPairs[i], assetTypes[y]) + if err != nil { + log.Errorf(log.SyncMgr, "failed to get item. Err: %s\n", err) + continue + } + if switchedToRest && usingWebsocket { + log.Warnf(log.SyncMgr, + "%s %s: Websocket re-enabled, switching from rest to websocket\n", + c.Exchange, m.FormatCurrency(enabledPairs[i]).String()) + switchedToRest = false + } + + if m.config.SyncOrderbook { + if !m.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemOrderbook) { + if c.Orderbook.LastUpdated.IsZero() || + (time.Since(c.Orderbook.LastUpdated) > m.config.SyncTimeoutREST && c.Orderbook.IsUsingREST) || + (time.Since(c.Orderbook.LastUpdated) > m.config.SyncTimeoutWebsocket && c.Orderbook.IsUsingWebsocket) { + if c.Orderbook.IsUsingWebsocket { + if time.Since(c.Created) < m.config.SyncTimeoutWebsocket { + continue + } + if supportsREST { + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true) + c.Orderbook.IsUsingWebsocket = false + c.Orderbook.IsUsingREST = true + log.Warnf(log.SyncMgr, + "%s %s %s: No orderbook update after %s, switching from websocket to rest\n", + c.Exchange, + m.FormatCurrency(c.Pair).String(), + strings.ToUpper(c.AssetType.String()), + m.config.SyncTimeoutREST, + ) + switchedToRest = true + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, false) + } + } + + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true) + result, err := exchanges[x].UpdateOrderbook(c.Pair, c.AssetType) + m.PrintOrderbookSummary(result, "REST", err) + if err == nil { + if m.remoteConfig.WebsocketRPC.Enabled { + relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), exchangeName) + } + } + updateErr := m.Update(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, err) + if updateErr != nil { + log.Error(log.SyncMgr, updateErr) + } + } else { + time.Sleep(time.Millisecond * 50) + } + } + + if m.config.SyncTicker { + if !m.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTicker) { + if c.Ticker.LastUpdated.IsZero() || + (time.Since(c.Ticker.LastUpdated) > m.config.SyncTimeoutREST && c.Ticker.IsUsingREST) || + (time.Since(c.Ticker.LastUpdated) > m.config.SyncTimeoutWebsocket && c.Ticker.IsUsingWebsocket) { + if c.Ticker.IsUsingWebsocket { + if time.Since(c.Created) < m.config.SyncTimeoutWebsocket { + continue + } + + if supportsREST { + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true) + c.Ticker.IsUsingWebsocket = false + c.Ticker.IsUsingREST = true + log.Warnf(log.SyncMgr, + "%s %s %s: No ticker update after %s, switching from websocket to rest\n", + c.Exchange, + m.FormatCurrency(enabledPairs[i]).String(), + strings.ToUpper(c.AssetType.String()), + m.config.SyncTimeoutWebsocket, + ) + switchedToRest = true + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, false) + } + } + + if c.Ticker.IsUsingREST { + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true) + var result *ticker.Price + var err error + + if supportsRESTTickerBatching { + m.mux.Lock() + batchLastDone, ok := m.tickerBatchLastRequested[exchangeName] + if !ok { + m.tickerBatchLastRequested[exchangeName] = time.Time{} + } + m.mux.Unlock() + + if batchLastDone.IsZero() || time.Since(batchLastDone) > m.config.SyncTimeoutREST { + m.mux.Lock() + if m.config.Verbose { + log.Debugf(log.SyncMgr, "%s Init'ing REST ticker batching\n", exchangeName) + } + result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType) + m.tickerBatchLastRequested[exchangeName] = time.Now() + m.mux.Unlock() + } else { + if m.config.Verbose { + log.Debugf(log.SyncMgr, "%s Using recent batching cache\n", exchangeName) + } + result, err = exchanges[x].FetchTicker(c.Pair, c.AssetType) + } + } else { + result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType) + } + m.PrintTickerSummary(result, "REST", err) + if err == nil { + if m.remoteConfig.WebsocketRPC.Enabled { + relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName) + } + } + updateErr := m.Update(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, err) + if updateErr != nil { + log.Error(log.SyncMgr, updateErr) + } + } + } else { + time.Sleep(time.Millisecond * 50) + } + } + } + + if m.config.SyncTrades { + if !m.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTrade) { + if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > m.config.SyncTimeoutREST { + m.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, true) + err := m.Update(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, nil) + if err != nil { + log.Error(log.SyncMgr, err) + } + } + } + } + } + } + } + } + } +} + +func printCurrencyFormat(price float64, displayCurrency currency.Code) string { + displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) + if err != nil { + log.Errorf(log.SyncMgr, "Failed to get display symbol: %s\n", err) + } + + return fmt.Sprintf("%s%.8f", displaySymbol, price) +} + +func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64, displayCurrency currency.Code) string { + conv, err := currency.ConvertCurrency(origPrice, + origCurrency, + displayCurrency) + if err != nil { + log.Errorf(log.SyncMgr, "Failed to convert currency: %s\n", err) + } + + displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) + if err != nil { + log.Errorf(log.SyncMgr, "Failed to get display symbol: %s\n", err) + } + + origSymbol, err := currency.GetSymbolByCurrencyName(origCurrency) + if err != nil { + log.Errorf(log.SyncMgr, "Failed to get original currency symbol for %s: %s\n", + origCurrency, + err) + } + + return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", + displaySymbol, + conv, + displayCurrency, + origSymbol, + origPrice, + origCurrency, + ) +} + +// PrintTickerSummary outputs the ticker results +func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string, err error) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return + } + if err != nil { + if err == common.ErrNotYetImplemented { + log.Warnf(log.SyncMgr, "Failed to get %s ticker. Error: %s\n", + protocol, + err) + return + } + log.Errorf(log.SyncMgr, "Failed to get %s ticker. Error: %s\n", + protocol, + err) + return + } + + // ignoring error as not all tickers have volume populated and error is not actionable + _ = stats.Add(result.ExchangeName, result.Pair, result.AssetType, result.Last, result.Volume) + + if result.Pair.Quote.IsFiatCurrency() && + result.Pair.Quote != m.fiatDisplayCurrency && + !m.fiatDisplayCurrency.IsEmpty() { + origCurrency := result.Pair.Quote.Upper() + log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n", + result.ExchangeName, + protocol, + m.FormatCurrency(result.Pair), + strings.ToUpper(result.AssetType.String()), + printConvertCurrencyFormat(origCurrency, result.Last, m.fiatDisplayCurrency), + printConvertCurrencyFormat(origCurrency, result.Ask, m.fiatDisplayCurrency), + printConvertCurrencyFormat(origCurrency, result.Bid, m.fiatDisplayCurrency), + printConvertCurrencyFormat(origCurrency, result.High, m.fiatDisplayCurrency), + printConvertCurrencyFormat(origCurrency, result.Low, m.fiatDisplayCurrency), + result.Volume) + } else { + if result.Pair.Quote.IsFiatCurrency() && + result.Pair.Quote == m.fiatDisplayCurrency && + !m.fiatDisplayCurrency.IsEmpty() { + log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n", + result.ExchangeName, + protocol, + m.FormatCurrency(result.Pair), + strings.ToUpper(result.AssetType.String()), + printCurrencyFormat(result.Last, m.fiatDisplayCurrency), + printCurrencyFormat(result.Ask, m.fiatDisplayCurrency), + printCurrencyFormat(result.Bid, m.fiatDisplayCurrency), + printCurrencyFormat(result.High, m.fiatDisplayCurrency), + printCurrencyFormat(result.Low, m.fiatDisplayCurrency), + result.Volume) + } else { + log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f\n", + result.ExchangeName, + protocol, + m.FormatCurrency(result.Pair), + strings.ToUpper(result.AssetType.String()), + result.Last, + result.Ask, + result.Bid, + result.High, + result.Low, + result.Volume) + } + } +} + +// FormatCurrency is a method that formats and returns a currency pair +// based on the user currency display preferences +func (m *syncManager) FormatCurrency(p currency.Pair) currency.Pair { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return p + } + return p.Format(m.delimiter, m.uppercase) +} + +const ( + book = "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n" +) + +// PrintOrderbookSummary outputs orderbook results +func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol string, err error) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return + } + if err != nil { + if result == nil { + log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n", + protocol, + err) + return + } + if err == common.ErrNotYetImplemented { + log.Warnf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n", + protocol, + result.Exchange, + result.Pair, + result.Asset, + err) + return + } + log.Errorf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n", + protocol, + result.Exchange, + result.Pair, + result.Asset, + err) + return + } + + bidsAmount, bidsValue := result.TotalBidsAmount() + asksAmount, asksValue := result.TotalAsksAmount() + + var bidValueResult, askValueResult string + switch { + case result.Pair.Quote.IsFiatCurrency() && result.Pair.Quote != m.fiatDisplayCurrency && !m.fiatDisplayCurrency.IsEmpty(): + origCurrency := result.Pair.Quote.Upper() + bidValueResult = printConvertCurrencyFormat(origCurrency, bidsValue, m.fiatDisplayCurrency) + askValueResult = printConvertCurrencyFormat(origCurrency, asksValue, m.fiatDisplayCurrency) + case result.Pair.Quote.IsFiatCurrency() && result.Pair.Quote == m.fiatDisplayCurrency && !m.fiatDisplayCurrency.IsEmpty(): + bidValueResult = printCurrencyFormat(bidsValue, m.fiatDisplayCurrency) + askValueResult = printCurrencyFormat(asksValue, m.fiatDisplayCurrency) + default: + bidValueResult = strconv.FormatFloat(bidsValue, 'f', -1, 64) + askValueResult = strconv.FormatFloat(asksValue, 'f', -1, 64) + } + + log.Infof(log.OrderBook, book, + result.Exchange, + protocol, + m.FormatCurrency(result.Pair), + strings.ToUpper(result.Asset.String()), + len(result.Bids), + bidsAmount, + result.Pair.Base, + bidValueResult, + len(result.Asks), + asksAmount, + result.Pair.Base, + askValueResult, + ) +} + +func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { + evt := WebsocketEvent{ + Data: result, + Event: event, + AssetType: assetType, + Exchange: exchangeName, + } + err := BroadcastWebsocketMessage(evt) + if !errors.Is(err, ErrWebsocketServiceNotRunning) { + log.Errorf(log.APIServerMgr, "Failed to broadcast websocket event %v. Error: %s\n", + event, err) + } +} diff --git a/engine/sync_manager.md b/engine/sync_manager.md new file mode 100644 index 00000000..0e01ad0d --- /dev/null +++ b/engine/sync_manager.md @@ -0,0 +1,56 @@ +# GoCryptoTrader package Sync_manager + + + + +[![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/sync_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This sync_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Sync_manager ++ The currency pair syncer subsystem is used to keep all trades, tickers and orderbooks up to date for all enabled exchange asset currency pairs ++ It can sync data via a websocket connection or REST and will switch between them if there has been no updates ++ In order to modify the behaviour of the currency pair syncer subsystem, you can change runtime parameters as detailed below: + +| Config | Description | Example | +| ------ | ----------- | ------- | +| syncmanager | Determines whether the subsystem is enabled | `true` | +| tickersync | Enables ticker syncing for all enabled exchanges | `true`| +| orderbooksync | Enables orderbook syncing for all enabled exchanges | `true` | +| tradesync | Enables trade syncing for all enabled exchanges | `true` | +| syncworkers | The amount of workers (goroutines) to use for syncing exchange data | `15` | +| synccontinuously | Whether to sync exchange data continuously (ticker, orderbook and trades) | `true` | +| synctimeout | The amount of time in golang `time.Duration` format before the syncer will switch from one protocol to the other (e.g. from REST to websocket) | `15000000000` | + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/sync_manager_test.go b/engine/sync_manager_test.go new file mode 100644 index 00000000..c23b98f5 --- /dev/null +++ b/engine/sync_manager_test.go @@ -0,0 +1,200 @@ +package engine + +import ( + "errors" + "sync/atomic" + "testing" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" +) + +func TestSetupSyncManager(t *testing.T) { + _, err := setupSyncManager(&Config{}, nil, nil, nil) + if !errors.Is(err, errNoSyncItemsEnabled) { + t.Errorf("error '%v', expected '%v'", err, errNoSyncItemsEnabled) + } + + _, err = setupSyncManager(&Config{SyncTrades: true}, nil, nil, nil) + if !errors.Is(err, errNilExchangeManager) { + t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) + } + + _, err = setupSyncManager(&Config{SyncTrades: true}, &ExchangeManager{}, nil, nil) + if !errors.Is(err, errNilConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilConfig) + } + + m, err := setupSyncManager(&Config{SyncTrades: true}, &ExchangeManager{}, nil, &config.RemoteControlConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expected manager") + } +} + +func TestSyncManagerStart(t *testing.T) { + m, err := setupSyncManager(&Config{SyncTrades: true}, &ExchangeManager{}, nil, &config.RemoteControlConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + em := SetupExchangeManager() + exch, err := em.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + exch.SetDefaults() + em.Add(exch) + m.exchangeManager = em + m.config.SyncContinuously = true + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } + + m = nil + err = m.Start() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } +} + +func TestSyncManagerStop(t *testing.T) { + var m *syncManager + err := m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + em := SetupExchangeManager() + exch, err := em.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + exch.SetDefaults() + em.Add(exch) + m, err = setupSyncManager(&Config{SyncTrades: true, SyncContinuously: true}, em, nil, &config.RemoteControlConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestPrintCurrencyFormat(t *testing.T) { + c := printCurrencyFormat(1337, currency.BTC) + if c == "" { + t.Error("expected formatted currency") + } +} + +func TestPrintConvertCurrencyFormat(t *testing.T) { + c := printConvertCurrencyFormat(currency.BTC, 1337, currency.USD) + if c == "" { + t.Error("expected formatted currency") + } +} + +func TestPrintTickerSummary(t *testing.T) { + var m *syncManager + m.PrintTickerSummary(&ticker.Price{}, "REST", nil) + + em := SetupExchangeManager() + exch, err := em.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + exch.SetDefaults() + em.Add(exch) + m, err = setupSyncManager(&Config{SyncTrades: true, SyncContinuously: true}, em, nil, &config.RemoteControlConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + atomic.StoreInt32(&m.started, 1) + m.PrintTickerSummary(&ticker.Price{ + Pair: currency.NewPair(currency.BTC, currency.USDT), + }, "REST", nil) + m.fiatDisplayCurrency = currency.USD + m.PrintTickerSummary(&ticker.Price{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", nil) + + m.fiatDisplayCurrency = currency.JPY + m.PrintTickerSummary(&ticker.Price{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", nil) + + m.PrintTickerSummary(&ticker.Price{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", errors.New("test")) + + m.PrintTickerSummary(&ticker.Price{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", common.ErrNotYetImplemented) +} + +func TestPrintOrderbookSummary(t *testing.T) { + var m *syncManager + m.PrintOrderbookSummary(nil, "REST", nil) + + em := SetupExchangeManager() + exch, err := em.NewExchangeByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + exch.SetDefaults() + em.Add(exch) + m, err = setupSyncManager(&Config{SyncTrades: true, SyncContinuously: true}, em, nil, &config.RemoteControlConfig{}) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + atomic.StoreInt32(&m.started, 1) + m.PrintOrderbookSummary(&orderbook.Base{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", nil) + + m.fiatDisplayCurrency = currency.USD + m.PrintOrderbookSummary(&orderbook.Base{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", nil) + + m.fiatDisplayCurrency = currency.JPY + m.PrintOrderbookSummary(&orderbook.Base{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", nil) + + m.PrintOrderbookSummary(&orderbook.Base{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", common.ErrNotYetImplemented) + + m.PrintOrderbookSummary(&orderbook.Base{ + Pair: currency.NewPair(currency.AUD, currency.USD), + }, "REST", errors.New("test")) + + m.PrintOrderbookSummary(nil, "REST", errors.New("test")) +} + +func TestRelayWebsocketEvent(t *testing.T) { + relayWebsocketEvent(nil, "", "", "") +} diff --git a/engine/sync_manager_types.go b/engine/sync_manager_types.go new file mode 100644 index 00000000..38121ad2 --- /dev/null +++ b/engine/sync_manager_types.go @@ -0,0 +1,64 @@ +package engine + +import ( + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// syncBase stores information +type syncBase struct { + IsUsingWebsocket bool + IsUsingREST bool + IsProcessing bool + LastUpdated time.Time + HaveData bool + NumErrors int +} + +// currencyPairSyncAgent stores the sync agent info +type currencyPairSyncAgent struct { + Created time.Time + Exchange string + AssetType asset.Item + Pair currency.Pair + Ticker syncBase + Orderbook syncBase + Trade syncBase +} + +// Config stores the currency pair config +type Config struct { + SyncTicker bool + SyncOrderbook bool + SyncTrades bool + SyncContinuously bool + SyncTimeoutREST time.Duration + SyncTimeoutWebsocket time.Duration + NumWorkers int + Verbose bool +} + +// syncManager stores the exchange currency pair syncer object +type syncManager struct { + initSyncCompleted int32 + initSyncStarted int32 + started int32 + delimiter string + uppercase bool + initSyncStartTime time.Time + fiatDisplayCurrency currency.Code + mux sync.Mutex + initSyncWG sync.WaitGroup + + currencyPairs []currencyPairSyncAgent + tickerBatchLastRequested map[string]time.Time + + remoteConfig *config.RemoteControlConfig + config Config + exchangeManager iExchangeManager + websocketDataReceiver iWebsocketDataReceiver +} diff --git a/engine/syncer.go b/engine/syncer.go deleted file mode 100644 index b14b68d2..00000000 --- a/engine/syncer.go +++ /dev/null @@ -1,635 +0,0 @@ -package engine - -import ( - "errors" - "strings" - "sync/atomic" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// const holds the sync item types -const ( - SyncItemTicker = iota - SyncItemOrderbook - SyncItemTrade - - DefaultSyncerWorkers = 15 - DefaultSyncerTimeoutREST = time.Second * 15 - DefaultSyncerTimeoutWebsocket = time.Minute -) - -var ( - createdCounter = 0 - removedCounter = 0 -) - -// NewCurrencyPairSyncer starts a new CurrencyPairSyncer -func NewCurrencyPairSyncer(c CurrencyPairSyncerConfig) (*ExchangeCurrencyPairSyncer, error) { - if !c.SyncOrderbook && !c.SyncTicker && !c.SyncTrades { - return nil, errors.New("no sync items enabled") - } - - if c.NumWorkers <= 0 { - c.NumWorkers = DefaultSyncerWorkers - } - - if c.SyncTimeoutREST <= time.Duration(0) { - c.SyncTimeoutREST = DefaultSyncerTimeoutREST - } - - if c.SyncTimeoutWebsocket <= time.Duration(0) { - c.SyncTimeoutWebsocket = DefaultSyncerTimeoutWebsocket - } - - s := ExchangeCurrencyPairSyncer{Cfg: c} - - s.tickerBatchLastRequested = make(map[string]time.Time) - - log.Debugf(log.SyncMgr, - "Exchange currency pair syncer config: continuous: %v ticker: %v"+ - " orderbook: %v trades: %v workers: %v verbose: %v timeout REST: %v"+ - " timeout Websocket: %v\n", - s.Cfg.SyncContinuously, s.Cfg.SyncTicker, s.Cfg.SyncOrderbook, - s.Cfg.SyncTrades, s.Cfg.NumWorkers, s.Cfg.Verbose, s.Cfg.SyncTimeoutREST, - s.Cfg.SyncTimeoutWebsocket) - return &s, nil -} - -func (e *ExchangeCurrencyPairSyncer) get(exchangeName string, p currency.Pair, a asset.Item) (*CurrencyPairSyncAgent, error) { - e.mux.Lock() - defer e.mux.Unlock() - - for x := range e.CurrencyPairs { - if e.CurrencyPairs[x].Exchange == exchangeName && - e.CurrencyPairs[x].Pair.Equal(p) && - e.CurrencyPairs[x].AssetType == a { - return &e.CurrencyPairs[x], nil - } - } - - return nil, errors.New("exchange currency pair syncer not found") -} - -func (e *ExchangeCurrencyPairSyncer) exists(exchangeName string, p currency.Pair, a asset.Item) bool { - e.mux.Lock() - defer e.mux.Unlock() - - for x := range e.CurrencyPairs { - if e.CurrencyPairs[x].Exchange == exchangeName && - e.CurrencyPairs[x].Pair.Equal(p) && - e.CurrencyPairs[x].AssetType == a { - return true - } - } - return false -} - -func (e *ExchangeCurrencyPairSyncer) add(c *CurrencyPairSyncAgent) { - e.mux.Lock() - defer e.mux.Unlock() - - if e.Cfg.SyncTicker { - if e.Cfg.Verbose { - log.Debugf(log.SyncMgr, - "%s: Added ticker sync item %v: using websocket: %v using REST: %v\n", - c.Exchange, Bot.FormatCurrency(c.Pair).String(), c.Ticker.IsUsingWebsocket, - c.Ticker.IsUsingREST) - } - if atomic.LoadInt32(&e.initSyncCompleted) != 1 { - e.initSyncWG.Add(1) - createdCounter++ - } - } - - if e.Cfg.SyncOrderbook { - if e.Cfg.Verbose { - log.Debugf(log.SyncMgr, - "%s: Added orderbook sync item %v: using websocket: %v using REST: %v\n", - c.Exchange, Bot.FormatCurrency(c.Pair).String(), c.Orderbook.IsUsingWebsocket, - c.Orderbook.IsUsingREST) - } - if atomic.LoadInt32(&e.initSyncCompleted) != 1 { - e.initSyncWG.Add(1) - createdCounter++ - } - } - - if e.Cfg.SyncTrades { - if e.Cfg.Verbose { - log.Debugf(log.SyncMgr, - "%s: Added trade sync item %v: using websocket: %v using REST: %v\n", - c.Exchange, Bot.FormatCurrency(c.Pair).String(), c.Trade.IsUsingWebsocket, - c.Trade.IsUsingREST) - } - if atomic.LoadInt32(&e.initSyncCompleted) != 1 { - e.initSyncWG.Add(1) - createdCounter++ - } - } - - c.Created = time.Now() - e.CurrencyPairs = append(e.CurrencyPairs, *c) -} - -func (e *ExchangeCurrencyPairSyncer) isProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int) bool { - e.mux.Lock() - defer e.mux.Unlock() - - for x := range e.CurrencyPairs { - if e.CurrencyPairs[x].Exchange == exchangeName && - e.CurrencyPairs[x].Pair.Equal(p) && - e.CurrencyPairs[x].AssetType == a { - switch syncType { - case SyncItemTicker: - return e.CurrencyPairs[x].Ticker.IsProcessing - case SyncItemOrderbook: - return e.CurrencyPairs[x].Orderbook.IsProcessing - case SyncItemTrade: - return e.CurrencyPairs[x].Trade.IsProcessing - } - } - } - - return false -} - -func (e *ExchangeCurrencyPairSyncer) setProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int, processing bool) { - e.mux.Lock() - defer e.mux.Unlock() - - for x := range e.CurrencyPairs { - if e.CurrencyPairs[x].Exchange == exchangeName && - e.CurrencyPairs[x].Pair.Equal(p) && - e.CurrencyPairs[x].AssetType == a { - switch syncType { - case SyncItemTicker: - e.CurrencyPairs[x].Ticker.IsProcessing = processing - case SyncItemOrderbook: - e.CurrencyPairs[x].Orderbook.IsProcessing = processing - case SyncItemTrade: - e.CurrencyPairs[x].Trade.IsProcessing = processing - } - } - } -} - -func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair, a asset.Item, syncType int, err error) { - if atomic.LoadInt32(&e.initSyncStarted) != 1 { - return - } - - switch syncType { - case SyncItemOrderbook: - if !e.Cfg.SyncOrderbook { - return - } - - case SyncItemTicker: - if !e.Cfg.SyncTicker { - return - } - - case SyncItemTrade: - if !e.Cfg.SyncTrades { - return - } - default: - log.Warnf(log.SyncMgr, "ExchangeCurrencyPairSyncer: unknown sync item %v\n", syncType) - return - } - - e.mux.Lock() - defer e.mux.Unlock() - - for x := range e.CurrencyPairs { - if e.CurrencyPairs[x].Exchange == exchangeName && - e.CurrencyPairs[x].Pair.Equal(p) && - e.CurrencyPairs[x].AssetType == a { - switch syncType { - case SyncItemTicker: - origHadData := e.CurrencyPairs[x].Ticker.HaveData - e.CurrencyPairs[x].Ticker.LastUpdated = time.Now() - if err != nil { - e.CurrencyPairs[x].Ticker.NumErrors++ - } - e.CurrencyPairs[x].Ticker.HaveData = true - e.CurrencyPairs[x].Ticker.IsProcessing = false - if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData { - removedCounter++ - log.Debugf(log.SyncMgr, "%s ticker sync complete %v [%d/%d].\n", - exchangeName, - Bot.FormatCurrency(p).String(), - removedCounter, - createdCounter) - e.initSyncWG.Done() - } - - case SyncItemOrderbook: - origHadData := e.CurrencyPairs[x].Orderbook.HaveData - e.CurrencyPairs[x].Orderbook.LastUpdated = time.Now() - if err != nil { - e.CurrencyPairs[x].Orderbook.NumErrors++ - } - e.CurrencyPairs[x].Orderbook.HaveData = true - e.CurrencyPairs[x].Orderbook.IsProcessing = false - if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData { - removedCounter++ - log.Debugf(log.SyncMgr, "%s orderbook sync complete %v [%d/%d].\n", - exchangeName, - Bot.FormatCurrency(p).String(), - removedCounter, - createdCounter) - e.initSyncWG.Done() - } - - case SyncItemTrade: - origHadData := e.CurrencyPairs[x].Trade.HaveData - e.CurrencyPairs[x].Trade.LastUpdated = time.Now() - if err != nil { - e.CurrencyPairs[x].Trade.NumErrors++ - } - e.CurrencyPairs[x].Trade.HaveData = true - e.CurrencyPairs[x].Trade.IsProcessing = false - if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData { - removedCounter++ - log.Debugf(log.SyncMgr, "%s trade sync complete %v [%d/%d].\n", - exchangeName, - Bot.FormatCurrency(p).String(), - removedCounter, - createdCounter) - e.initSyncWG.Done() - } - } - } - } -} - -func (e *ExchangeCurrencyPairSyncer) worker() { - cleanup := func() { - log.Debugln(log.SyncMgr, - "Exchange CurrencyPairSyncer worker shutting down.") - } - defer cleanup() - - for atomic.LoadInt32(&e.shutdown) != 1 { - exchanges := Bot.GetExchanges() - for x := range exchanges { - exchangeName := exchanges[x].GetName() - assetTypes := exchanges[x].GetAssetTypes() - supportsREST := exchanges[x].SupportsREST() - supportsRESTTickerBatching := exchanges[x].SupportsRESTTickerBatchUpdates() - var usingREST bool - var usingWebsocket bool - var switchedToRest bool - if exchanges[x].SupportsWebsocket() && exchanges[x].IsWebsocketEnabled() { - ws, err := exchanges[x].GetWebsocket() - if err != nil { - log.Errorf(log.SyncMgr, - "%s unable to get websocket pointer. Err: %s\n", - exchangeName, - err) - usingREST = true - } - - if ws.IsConnected() { - usingWebsocket = true - } else { - usingREST = true - } - } else if supportsREST { - usingREST = true - } - - for y := range assetTypes { - if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil { - continue - } - - wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y]) - enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y]) - if err != nil { - log.Errorf(log.SyncMgr, - "%s failed to get enabled pairs. Err: %s\n", - exchangeName, - err) - continue - } - for i := range enabledPairs { - if atomic.LoadInt32(&e.shutdown) == 1 { - return - } - - if !e.exists(exchangeName, enabledPairs[i], assetTypes[y]) { - c := CurrencyPairSyncAgent{ - AssetType: assetTypes[y], - Exchange: exchangeName, - Pair: enabledPairs[i], - } - - sBase := SyncBase{ - IsUsingREST: usingREST || !wsAssetSupported, - IsUsingWebsocket: usingWebsocket && wsAssetSupported, - } - - if e.Cfg.SyncTicker { - c.Ticker = sBase - } - - if e.Cfg.SyncOrderbook { - c.Orderbook = sBase - } - - if e.Cfg.SyncTrades { - c.Trade = sBase - } - - e.add(&c) - } - - c, err := e.get(exchangeName, enabledPairs[i], assetTypes[y]) - if err != nil { - log.Errorf(log.SyncMgr, "failed to get item. Err: %s\n", err) - continue - } - if switchedToRest && usingWebsocket { - log.Warnf(log.SyncMgr, - "%s %s: Websocket re-enabled, switching from rest to websocket\n", - c.Exchange, Bot.FormatCurrency(enabledPairs[i]).String()) - switchedToRest = false - } - if e.Cfg.SyncTicker { - if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTicker) { - if c.Ticker.LastUpdated.IsZero() || - (time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeoutREST && c.Ticker.IsUsingREST) || - (time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeoutWebsocket && c.Ticker.IsUsingWebsocket) { - if c.Ticker.IsUsingWebsocket { - if time.Since(c.Created) < e.Cfg.SyncTimeoutWebsocket { - continue - } - - if supportsREST { - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true) - c.Ticker.IsUsingWebsocket = false - c.Ticker.IsUsingREST = true - log.Warnf(log.SyncMgr, - "%s %s %s: No ticker update after %s, switching from websocket to rest\n", - c.Exchange, - Bot.FormatCurrency(enabledPairs[i]).String(), - strings.ToUpper(c.AssetType.String()), - e.Cfg.SyncTimeoutWebsocket, - ) - switchedToRest = true - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, false) - } - } - - if c.Ticker.IsUsingREST { - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true) - var result *ticker.Price - var err error - - if supportsRESTTickerBatching { - e.mux.Lock() - batchLastDone, ok := e.tickerBatchLastRequested[exchangeName] - if !ok { - e.tickerBatchLastRequested[exchangeName] = time.Time{} - } - e.mux.Unlock() - - if batchLastDone.IsZero() || time.Since(batchLastDone) > e.Cfg.SyncTimeoutREST { - e.mux.Lock() - if e.Cfg.Verbose { - log.Debugf(log.SyncMgr, "%s Init'ing REST ticker batching\n", exchangeName) - } - result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType) - e.tickerBatchLastRequested[exchangeName] = time.Now() - e.mux.Unlock() - } else { - if e.Cfg.Verbose { - log.Debugf(log.SyncMgr, "%s Using recent batching cache\n", exchangeName) - } - result, err = exchanges[x].FetchTicker(c.Pair, c.AssetType) - } - } else { - result, err = exchanges[x].UpdateTicker(c.Pair, c.AssetType) - } - printTickerSummary(result, "REST", err) - if err == nil { - if Bot.Config.RemoteControl.WebsocketRPC.Enabled { - relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName) - } - } - e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, err) - } - } else { - time.Sleep(time.Millisecond * 50) - } - } - } - - if e.Cfg.SyncOrderbook { - if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemOrderbook) { - if c.Orderbook.LastUpdated.IsZero() || - (time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeoutREST && c.Orderbook.IsUsingREST) || - (time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeoutWebsocket && c.Orderbook.IsUsingWebsocket) { - if c.Orderbook.IsUsingWebsocket { - if time.Since(c.Created) < e.Cfg.SyncTimeoutWebsocket { - continue - } - if supportsREST { - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true) - c.Orderbook.IsUsingWebsocket = false - c.Orderbook.IsUsingREST = true - log.Warnf(log.SyncMgr, - "%s %s %s: No orderbook update after %s, switching from websocket to rest\n", - c.Exchange, - Bot.FormatCurrency(c.Pair).String(), - strings.ToUpper(c.AssetType.String()), - e.Cfg.SyncTimeoutWebsocket, - ) - switchedToRest = true - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, false) - } - } - - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true) - result, err := exchanges[x].UpdateOrderbook(c.Pair, c.AssetType) - printOrderbookSummary(result, "REST", Bot, err) - if err == nil { - if Bot.Config.RemoteControl.WebsocketRPC.Enabled { - relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), exchangeName) - } - } - e.update(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, err) - } else { - time.Sleep(time.Millisecond * 50) - } - } - if e.Cfg.SyncTrades { - if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTrade) { - if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > e.Cfg.SyncTimeoutREST { - e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, true) - e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, nil) - } - } - } - } - } - } - } - } -} - -// Start starts an exchange currency pair syncer -func (e *ExchangeCurrencyPairSyncer) Start() { - log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer started.") - exchanges := Bot.GetExchanges() - for x := range exchanges { - exchangeName := exchanges[x].GetName() - supportsWebsocket := exchanges[x].SupportsWebsocket() - assetTypes := exchanges[x].GetAssetTypes() - supportsREST := exchanges[x].SupportsREST() - - if !supportsREST && !supportsWebsocket { - log.Warnf(log.SyncMgr, - "Loaded exchange %s does not support REST or Websocket.\n", - exchangeName) - continue - } - - var usingWebsocket bool - var usingREST bool - if supportsWebsocket && exchanges[x].IsWebsocketEnabled() { - ws, err := exchanges[x].GetWebsocket() - if err != nil { - log.Errorf(log.SyncMgr, - "%s failed to get websocket. Err: %s\n", - exchangeName, - err) - usingREST = true - } - - if !ws.IsConnected() && !ws.IsConnecting() { - go Bot.WebsocketDataReceiver(ws) - - err = ws.Connect() - if err == nil { - err = ws.FlushChannels() - } - if err != nil { - log.Errorf(log.SyncMgr, - "%s websocket failed to connect. Err: %s\n", - exchangeName, - err) - usingREST = true - } else { - usingWebsocket = true - } - } else { - usingWebsocket = true - } - } else if supportsREST { - usingREST = true - } - - for y := range assetTypes { - if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil { - log.Warnf(log.SyncMgr, - "%s asset type %s is disabled, fetching enabled pairs is paused", - exchangeName, - assetTypes[y]) - continue - } - - wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y]) - if !wsAssetSupported { - log.Warnf(log.SyncMgr, - "%s asset type %s websocket functionality is unsupported, REST fetching only.", - exchangeName, - assetTypes[y]) - } - enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y]) - if err != nil { - log.Errorf(log.SyncMgr, - "%s failed to get enabled pairs. Err: %s\n", - exchangeName, - err) - continue - } - for i := range enabledPairs { - if e.exists(exchangeName, enabledPairs[i], assetTypes[y]) { - continue - } - - c := CurrencyPairSyncAgent{ - AssetType: assetTypes[y], - Exchange: exchangeName, - Pair: enabledPairs[i], - } - - sBase := SyncBase{ - IsUsingREST: usingREST || !wsAssetSupported, - IsUsingWebsocket: usingWebsocket && wsAssetSupported, - } - - if e.Cfg.SyncTicker { - c.Ticker = sBase - } - - if e.Cfg.SyncOrderbook { - c.Orderbook = sBase - } - - if e.Cfg.SyncTrades { - c.Trade = sBase - } - - e.add(&c) - } - } - } - - if atomic.CompareAndSwapInt32(&e.initSyncStarted, 0, 1) { - log.Debugf(log.SyncMgr, - "Exchange CurrencyPairSyncer initial sync started. %d items to process.\n", - createdCounter) - e.initSyncStartTime = time.Now() - } - - go func() { - e.initSyncWG.Wait() - if atomic.CompareAndSwapInt32(&e.initSyncCompleted, 0, 1) { - log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync is complete.\n") - completedTime := time.Now() - log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync took %v [%v sync items].\n", - completedTime.Sub(e.initSyncStartTime), createdCounter) - - if !e.Cfg.SyncContinuously { - log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopping.") - e.Stop() - return - } - } - }() - - if atomic.LoadInt32(&e.initSyncCompleted) == 1 && !e.Cfg.SyncContinuously { - return - } - - for i := 0; i < e.Cfg.NumWorkers; i++ { - go e.worker() - } -} - -// Stop shuts down the exchange currency pair syncer -func (e *ExchangeCurrencyPairSyncer) Stop() { - stopped := atomic.CompareAndSwapInt32(&e.shutdown, 0, 1) - if stopped { - log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopped.") - } -} diff --git a/engine/syncer_test.go b/engine/syncer_test.go deleted file mode 100644 index f7e1f872..00000000 --- a/engine/syncer_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package engine - -import ( - "testing" - "time" - - "github.com/thrasher-corp/gocryptotrader/config" -) - -func TestNewCurrencyPairSyncer(t *testing.T) { - t.Skip() - - if Bot == nil { - Bot = new(Engine) - } - Bot.Config = &config.Cfg - err := Bot.Config.LoadConfig("", true) - if err != nil { - t.Fatalf("TestNewExchangeSyncer: Failed to load config: %s", err) - } - - Bot.Settings.DisableExchangeAutoPairUpdates = true - Bot.Settings.EnableExchangeWebsocketSupport = true - - err = Bot.SetupExchanges() - if err != nil { - t.Log(err) - } - - Bot.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(CurrencyPairSyncerConfig{ - SyncTicker: true, - SyncOrderbook: false, - SyncTrades: false, - SyncContinuously: false, - }) - if err != nil { - t.Errorf("NewCurrencyPairSyncer failed: err %s", err) - } - - Bot.ExchangeCurrencyPairManager.Start() - time.Sleep(time.Second * 15) - Bot.ExchangeCurrencyPairManager.Stop() -} diff --git a/engine/syncer_types.go b/engine/syncer_types.go deleted file mode 100644 index 3a3c0f5e..00000000 --- a/engine/syncer_types.go +++ /dev/null @@ -1,62 +0,0 @@ -package engine - -import ( - "sync" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" -) - -// CurrencyPairSyncerConfig stores the currency pair config -type CurrencyPairSyncerConfig struct { - SyncTicker bool - SyncOrderbook bool - SyncTrades bool - SyncContinuously bool - SyncTimeoutREST time.Duration - SyncTimeoutWebsocket time.Duration - NumWorkers int - Verbose bool -} - -// ExchangeSyncerConfig stores the exchange syncer config -type ExchangeSyncerConfig struct { - SyncDepositAddresses bool - SyncOrders bool -} - -// ExchangeCurrencyPairSyncer stores the exchange currency pair syncer object -type ExchangeCurrencyPairSyncer struct { - Cfg CurrencyPairSyncerConfig - CurrencyPairs []CurrencyPairSyncAgent - tickerBatchLastRequested map[string]time.Time - mux sync.Mutex - initSyncWG sync.WaitGroup - - initSyncCompleted int32 - initSyncStarted int32 - initSyncStartTime time.Time - shutdown int32 -} - -// SyncBase stores information -type SyncBase struct { - IsUsingWebsocket bool - IsUsingREST bool - IsProcessing bool - LastUpdated time.Time - HaveData bool - NumErrors int -} - -// CurrencyPairSyncAgent stores the sync agent info -type CurrencyPairSyncAgent struct { - Created time.Time - Exchange string - AssetType asset.Item - Pair currency.Pair - Ticker SyncBase - Orderbook SyncBase - Trade SyncBase -} diff --git a/engine/timekeeper.go b/engine/timekeeper.go deleted file mode 100644 index 27d5583b..00000000 --- a/engine/timekeeper.go +++ /dev/null @@ -1,135 +0,0 @@ -package engine - -import ( - "errors" - "fmt" - "os" - "sync/atomic" - "time" - - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" - "github.com/thrasher-corp/gocryptotrader/log" - ntpclient "github.com/thrasher-corp/gocryptotrader/ntpclient" -) - -// vars related to the NTP manager -var ( - NTPCheckInterval = time.Second * 30 - NTPRetryLimit = 3 - errNTPDisabled = errors.New("ntp client disabled") -) - -// ntpManager starts the NTP manager -type ntpManager struct { - started int32 - initialCheck bool - shutdown chan struct{} -} - -func (n *ntpManager) Started() bool { - return atomic.LoadInt32(&n.started) == 1 -} - -func (n *ntpManager) Start() error { - if !atomic.CompareAndSwapInt32(&n.started, 0, 1) { - return fmt.Errorf("NTP manager %w", subsystem.ErrSubSystemAlreadyStarted) - } - - if Bot.Config.NTPClient.Level == -1 { - atomic.CompareAndSwapInt32(&n.started, 1, 0) - return errors.New("NTP client disabled") - } - - log.Debugln(log.TimeMgr, "NTP manager starting...") - if Bot.Config.NTPClient.Level == 0 && *Bot.Config.Logging.Enabled { - // Initial NTP check (prompts user on how we should proceed) - n.initialCheck = true - // Sometimes the NTP client can have transient issues due to UDP, try - // the default retry limits before giving up - check: - for i := 0; i < NTPRetryLimit; i++ { - err := n.processTime() - switch err { - case nil: - break check - case errNTPDisabled: - log.Debugln(log.TimeMgr, "NTP manager: User disabled NTP prompts. Exiting.") - atomic.CompareAndSwapInt32(&n.started, 1, 0) - return nil - default: - if i == NTPRetryLimit-1 { - return err - } - } - } - } - n.shutdown = make(chan struct{}) - go n.run() - log.Debugln(log.TimeMgr, "NTP manager started.") - return nil -} - -func (n *ntpManager) Stop() error { - if atomic.LoadInt32(&n.started) == 0 { - return fmt.Errorf("NTP manager %w", subsystem.ErrSubSystemNotStarted) - } - defer func() { - atomic.CompareAndSwapInt32(&n.started, 1, 0) - }() - log.Debugln(log.TimeMgr, "NTP manager shutting down...") - close(n.shutdown) - return nil -} - -func (n *ntpManager) run() { - t := time.NewTicker(NTPCheckInterval) - defer func() { - t.Stop() - log.Debugln(log.TimeMgr, "NTP manager shutdown.") - }() - - for { - select { - case <-n.shutdown: - return - case <-t.C: - err := n.processTime() - if err != nil { - log.Error(log.TimeMgr, err) - } - } - } -} - -func (n *ntpManager) FetchNTPTime() time.Time { - return ntpclient.NTPClient(Bot.Config.NTPClient.Pool) -} - -func (n *ntpManager) processTime() error { - NTPTime := n.FetchNTPTime() - currentTime := time.Now() - diff := NTPTime.Sub(currentTime) - configNTPTime := *Bot.Config.NTPClient.AllowedDifference - negDiff := *Bot.Config.NTPClient.AllowedNegativeDifference - configNTPNegativeTime := -negDiff - if diff > configNTPTime || diff < configNTPNegativeTime { - log.Warnf(log.TimeMgr, "NTP manager: Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v\n", - NTPTime, - currentTime, - diff, - configNTPTime, - configNTPNegativeTime) - if n.initialCheck { - n.initialCheck = false - disable, err := Bot.Config.DisableNTPCheck(os.Stdin) - if err != nil { - return fmt.Errorf("unable to disable NTP check: %s", err) - } - log.Infoln(log.TimeMgr, disable) - if Bot.Config.NTPClient.Level == -1 { - return errNTPDisabled - } - } - } - return nil -} diff --git a/engine/websocket.go b/engine/websocket.go deleted file mode 100644 index 7b57bca1..00000000 --- a/engine/websocket.go +++ /dev/null @@ -1,434 +0,0 @@ -package engine - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common/crypto" - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/log" -) - -// Const vars for websocket -const ( - WebsocketResponseSuccess = "OK" -) - -var ( - wsHub *WebsocketHub - wsHubStarted bool -) - -type wsCommandHandler struct { - authRequired bool - handler func(client *WebsocketClient, data interface{}) error -} - -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}, -} - -// 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.WebsocketMgr, "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.WebsocketMgr, "websocket: disconnected client") - close(client.Send) - delete(h.Clients, client) - } - } - } - } -} - -// SendWebsocketMessage sends a websocket event to the client -func (c *WebsocketClient) SendWebsocketMessage(evt interface{}) error { - data, err := json.Marshal(evt) - if err != nil { - log.Errorf(log.WebsocketMgr, "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 - c.Conn.Close() - }() - - for { - msgType, message, err := c.Conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Errorf(log.WebsocketMgr, "websocket: client disconnected, err: %s\n", err) - } - break - } - - if msgType == websocket.TextMessage { - var evt WebsocketEvent - err := json.Unmarshal(message, &evt) - if err != nil { - log.Errorf(log.WebsocketMgr, "websocket: failed to decode JSON sent from client %s\n", err) - continue - } - - if evt.Event == "" { - log.Warnln(log.WebsocketMgr, "websocket: client sent a blank event, disconnecting") - continue - } - - dataJSON, err := json.Marshal(evt.Data) - if err != nil { - log.Errorln(log.WebsocketMgr, "websocket: client sent data we couldn't JSON decode") - break - } - - req := strings.ToLower(evt.Event) - log.Debugf(log.WebsocketMgr, "websocket: request received: %s\n", req) - - result, ok := wsHandlers[req] - if !ok { - log.Debugln(log.WebsocketMgr, "websocket: unsupported event") - continue - } - - if result.authRequired && !c.Authenticated { - log.Warnf(log.WebsocketMgr, "Websocket: request %s failed due to unauthenticated request on an authenticated API\n", evt.Event) - c.SendWebsocketMessage(WebsocketEventResponse{Event: evt.Event, Error: "unauthorised request on authenticated API"}) - continue - } - - err = result.handler(c, dataJSON) - if err != nil { - log.Errorf(log.WebsocketMgr, "websocket: request %s failed. Error %s\n", evt.Event, err) - continue - } - } - } -} - -func (c *WebsocketClient) write() { - defer func() { - c.Conn.Close() - }() - for { // nolint // ws client write routine loop - select { - case message, ok := <-c.Send: - if !ok { - c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) - log.Debugln(log.WebsocketMgr, "websocket: hub closed the channel") - return - } - - w, err := c.Conn.NextWriter(websocket.TextMessage) - if err != nil { - log.Errorf(log.WebsocketMgr, "websocket: failed to create new io.writeCloser: %s\n", err) - return - } - w.Write(message) - - // Add queued chat messages to the current websocket message - n := len(c.Send) - for i := 0; i < n; i++ { - w.Write(<-c.Send) - } - - if err := w.Close(); err != nil { - log.Errorf(log.WebsocketMgr, "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 errors.New("websocket service not started") - } - - 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 WebsocketClientHandler(w http.ResponseWriter, r *http.Request) { - if !wsHubStarted { - StartWebsocketHandler() - } - - connectionLimit := Bot.Config.RemoteControl.WebsocketRPC.ConnectionLimit - numClients := len(wsHub.Clients) - - if numClients >= connectionLimit { - log.Warnf(log.WebsocketMgr, - "websocket: client rejected due to websocket client limit reached. Number of clients %d. Limit %d.\n", - numClients, connectionLimit) - w.WriteHeader(http.StatusForbidden) - return - } - - upgrader := websocket.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 Bot.Config.RemoteControl.WebsocketRPC.AllowInsecureOrigin { - upgrader.CheckOrigin = func(r *http.Request) bool { return true } - } - - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Error(log.WebsocketMgr, err) - return - } - - client := &WebsocketClient{Hub: wsHub, Conn: conn, Send: make(chan []byte, 1024)} - client.Hub.Register <- client - log.Debugf(log.WebsocketMgr, - "websocket: client connected. Connected clients: %d. Limit %d.\n", - numClients+1, connectionLimit) - - go client.read() - go client.write() -} - -func wsAuth(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "auth", - } - - var auth WebsocketAuth - err := json.Unmarshal(data.([]byte), &auth) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - - hashPW := crypto.HexEncodeToString(crypto.GetSHA256([]byte(Bot.Config.RemoteControl.Password))) - if auth.Username == Bot.Config.RemoteControl.Username && auth.Password == hashPW { - client.Authenticated = true - wsResp.Data = WebsocketResponseSuccess - log.Debugln(log.WebsocketMgr, - "websocket: client authenticated successfully") - return client.SendWebsocketMessage(wsResp) - } - - wsResp.Error = "invalid username/password" - client.authFailures++ - client.SendWebsocketMessage(wsResp) - if client.authFailures >= Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures { - log.Debugf(log.WebsocketMgr, - "websocket: disconnecting client, maximum auth failures threshold reached (failures: %d limit: %d)\n", - client.authFailures, Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures) - wsHub.Unregister <- client - return nil - } - - log.Debugf(log.WebsocketMgr, - "websocket: client sent wrong username/password (failures: %d limit: %d)\n", - client.authFailures, Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures) - return nil -} - -func wsGetConfig(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "GetConfig", - Data: Bot.Config, - } - return client.SendWebsocketMessage(wsResp) -} - -func wsSaveConfig(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "SaveConfig", - } - var cfg config.Config - err := json.Unmarshal(data.([]byte), &cfg) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - - err = Bot.Config.UpdateConfig(Bot.Settings.ConfigFile, &cfg, Bot.Settings.EnableDryRun) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - - Bot.SetupExchanges() - wsResp.Data = WebsocketResponseSuccess - return client.SendWebsocketMessage(wsResp) -} - -func wsGetAccountInfo(client *WebsocketClient, data interface{}) error { - accountInfo := Bot.GetAllEnabledExchangeAccountInfo() - wsResp := WebsocketEventResponse{ - Event: "GetAccountInfo", - Data: accountInfo, - } - return client.SendWebsocketMessage(wsResp) -} - -func wsGetTickers(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "GetTickers", - } - wsResp.Data = Bot.GetAllActiveTickers() - return client.SendWebsocketMessage(wsResp) -} - -func wsGetTicker(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "GetTicker", - } - var tickerReq WebsocketOrderbookTickerRequest - err := json.Unmarshal(data.([]byte), &tickerReq) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - - p, err := currency.NewPairFromString(tickerReq.Currency) - if err != nil { - return err - } - - a, err := asset.New(tickerReq.AssetType) - if err != nil { - return err - } - - result, err := Bot.GetSpecificTicker(p, tickerReq.Exchange, a) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - wsResp.Data = result - return client.SendWebsocketMessage(wsResp) -} - -func wsGetOrderbooks(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "GetOrderbooks", - } - wsResp.Data = GetAllActiveOrderbooks() - return client.SendWebsocketMessage(wsResp) -} - -func wsGetOrderbook(client *WebsocketClient, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "GetOrderbook", - } - var orderbookReq WebsocketOrderbookTickerRequest - err := json.Unmarshal(data.([]byte), &orderbookReq) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - - p, err := currency.NewPairFromString(orderbookReq.Currency) - if err != nil { - return err - } - - a, err := asset.New(orderbookReq.AssetType) - if err != nil { - return err - } - - result, err := Bot.GetSpecificOrderbook(p, orderbookReq.Exchange, a) - if err != nil { - wsResp.Error = err.Error() - client.SendWebsocketMessage(wsResp) - return err - } - wsResp.Data = result - return client.SendWebsocketMessage(wsResp) -} - -func wsGetExchangeRates(client *WebsocketClient, data interface{}) 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, data interface{}) error { - wsResp := WebsocketEventResponse{ - Event: "GetPortfolio", - } - wsResp.Data = Bot.Portfolio.GetPortfolioSummary() - return client.SendWebsocketMessage(wsResp) -} diff --git a/engine/websocket_types.go b/engine/websocket_types.go deleted file mode 100644 index c74483fc..00000000 --- a/engine/websocket_types.go +++ /dev/null @@ -1,49 +0,0 @@ -package engine - -import "github.com/gorilla/websocket" - -// WebsocketClient stores information related to the websocket client -type WebsocketClient struct { - Hub *WebsocketHub - Conn *websocket.Conn - Authenticated bool - authFailures int - Send chan []byte -} - -// 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 interface{} -} - -// WebsocketEventResponse is the struct used for websocket event responses -type WebsocketEventResponse struct { - Event string `json:"event"` - Data interface{} `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"` -} diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go new file mode 100644 index 00000000..0702005d --- /dev/null +++ b/engine/websocketroutine_manager.go @@ -0,0 +1,332 @@ +package engine + +import ( + "fmt" + "sync/atomic" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/log" +) + +// setupWebsocketRoutineManager creates a new websocket routine manager +func setupWebsocketRoutineManager(exchangeManager iExchangeManager, orderManager iOrderManager, syncer iCurrencyPairSyncer, cfg *config.CurrencyConfig, verbose bool) (*websocketRoutineManager, error) { + if exchangeManager == nil { + return nil, errNilExchangeManager + } + if orderManager == nil { + return nil, errNilOrderManager + } + if syncer == nil { + return nil, errNilCurrencyPairSyncer + } + if cfg == nil { + return nil, errNilCurrencyConfig + } + if cfg.CurrencyPairFormat == nil && verbose { + return nil, errNilCurrencyPairFormat + } + return &websocketRoutineManager{ + verbose: verbose, + exchangeManager: exchangeManager, + orderManager: orderManager, + syncer: syncer, + currencyConfig: cfg, + shutdown: make(chan struct{}), + }, nil +} + +// Start runs the subsystem +func (m *websocketRoutineManager) Start() error { + if m == nil { + return fmt.Errorf("websocket routine manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { + return ErrSubSystemAlreadyStarted + } + m.shutdown = make(chan struct{}) + go m.websocketRoutine() + return nil +} + +// IsRunning safely checks whether the subsystem is running +func (m *websocketRoutineManager) IsRunning() bool { + if m == nil { + return false + } + return atomic.LoadInt32(&m.started) == 1 +} + +// Stop attempts to shutdown the subsystem +func (m *websocketRoutineManager) Stop() error { + if m == nil { + return fmt.Errorf("websocket routine manager %w", ErrNilSubsystem) + } + if !atomic.CompareAndSwapInt32(&m.started, 1, 0) { + return fmt.Errorf("websocket routine manager %w", ErrSubSystemNotStarted) + } + close(m.shutdown) + m.wg.Wait() + return nil +} + +// websocketRoutine Initial routine management system for websocket +func (m *websocketRoutineManager) websocketRoutine() { + if m.verbose { + log.Debugln(log.WebsocketMgr, "Connecting exchange websocket services...") + } + exchanges := m.exchangeManager.GetExchanges() + for i := range exchanges { + go func(i int) { + if exchanges[i].SupportsWebsocket() { + if m.verbose { + log.Debugf(log.WebsocketMgr, + "Exchange %s websocket support: Yes Enabled: %v\n", + exchanges[i].GetName(), + common.IsEnabled(exchanges[i].IsWebsocketEnabled()), + ) + } + + ws, err := exchanges[i].GetWebsocket() + if err != nil { + log.Errorf( + log.WebsocketMgr, + "Exchange %s GetWebsocket error: %s\n", + exchanges[i].GetName(), + err, + ) + return + } + + // Exchange sync manager might have already started ws + // service or is in the process of connecting, so check + if ws.IsConnected() || ws.IsConnecting() { + return + } + + // Data handler routine + go m.WebsocketDataReceiver(ws) + + if ws.IsEnabled() { + err = ws.Connect() + if err != nil { + log.Errorf(log.WebsocketMgr, "%v\n", err) + } + err = ws.FlushChannels() + if err != nil { + log.Errorf(log.WebsocketMgr, "Failed to subscribe: %v\n", err) + } + } + } else if m.verbose { + log.Debugf(log.WebsocketMgr, + "Exchange %s websocket support: No\n", + exchanges[i].GetName(), + ) + } + }(i) + } +} + +// WebsocketDataReceiver handles websocket data coming from a websocket feed +// associated with an exchange +func (m *websocketRoutineManager) WebsocketDataReceiver(ws *stream.Websocket) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return + } + m.wg.Add(1) + defer m.wg.Done() + + for { + select { + case <-m.shutdown: + return + case data := <-ws.ToRoutine: + err := m.WebsocketDataHandler(ws.GetName(), data) + if err != nil { + log.Error(log.WebsocketMgr, err) + } + } + } +} + +// WebsocketDataHandler is a central point for exchange websocket implementations to send +// processed data. WebsocketDataHandler will then pass that to an appropriate handler +func (m *websocketRoutineManager) WebsocketDataHandler(exchName string, data interface{}) error { + if data == nil { + return fmt.Errorf("exchange %s nil data sent to websocket", + exchName) + } + + switch d := data.(type) { + case string: + log.Info(log.WebsocketMgr, d) + case error: + return fmt.Errorf("exchange %s websocket error - %s", exchName, data) + case stream.FundingData: + if m.verbose { + log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v", + exchName, + m.FormatCurrency(d.CurrencyPair), + d.AssetType, + d) + } + case *ticker.Price: + if m.syncer.IsRunning() { + err := m.syncer.Update(exchName, + d.Pair, + d.AssetType, + SyncItemTicker, + nil) + if err != nil { + return err + } + } + err := ticker.ProcessTicker(d) + if err != nil { + return err + } + m.syncer.PrintTickerSummary(d, "websocket", err) + case stream.KlineData: + if m.verbose { + log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", + exchName, + m.FormatCurrency(d.Pair), + d.AssetType, + d) + } + case *orderbook.Base: + if m.syncer.IsRunning() { + err := m.syncer.Update(exchName, + d.Pair, + d.Asset, + SyncItemOrderbook, + nil) + if err != nil { + return err + } + } + m.syncer.PrintOrderbookSummary(d, "websocket", nil) + case *order.Detail: + m.printOrderSummary(d) + if !m.orderManager.Exists(d) { + err := m.orderManager.Add(d) + if err != nil { + return err + } + } else { + od, err := m.orderManager.GetByExchangeAndID(d.Exchange, d.ID) + if err != nil { + return err + } + od.UpdateOrderFromDetail(d) + + err = m.orderManager.UpdateExistingOrder(od) + if err != nil { + return err + } + } + case *order.Modify: + m.printOrderChangeSummary(d) + od, err := m.orderManager.GetByExchangeAndID(d.Exchange, d.ID) + if err != nil { + return err + } + od.UpdateOrderFromModify(d) + err = m.orderManager.UpdateExistingOrder(od) + if err != nil { + return err + } + case order.ClassificationError: + return fmt.Errorf("%w %s", d.Err, d.Error()) + case stream.UnhandledMessageWarning: + log.Warn(log.WebsocketMgr, d.Message) + case account.Change: + if m.verbose { + m.printAccountHoldingsChangeSummary(d) + } + default: + if m.verbose { + log.Warnf(log.WebsocketMgr, + "%s websocket Unknown type: %+v", + exchName, + d) + } + } + return nil +} + +// FormatCurrency is a method that formats and returns a currency pair +// based on the user currency display preferences +func (m *websocketRoutineManager) FormatCurrency(p currency.Pair) currency.Pair { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return p + } + return p.Format(m.currencyConfig.CurrencyPairFormat.Delimiter, + m.currencyConfig.CurrencyPairFormat.Uppercase) +} + +// printOrderChangeSummary this function will be deprecated when a order manager +// update is done. +func (m *websocketRoutineManager) printOrderChangeSummary(o *order.Modify) { + if m == nil || atomic.LoadInt32(&m.started) == 0 || o == nil { + return + } + + log.Debugf(log.WebsocketMgr, + "Order Change: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f", + o.Exchange, + o.AssetType, + o.Pair, + o.Status, + o.Type, + o.Side, + o.ID, + o.ClientOrderID, + o.Price, + o.Amount, + o.ExecutedAmount, + o.RemainingAmount) +} + +// printOrderSummary this function will be deprecated when a order manager +// update is done. +func (m *websocketRoutineManager) printOrderSummary(o *order.Detail) { + if m == nil || atomic.LoadInt32(&m.started) == 0 || o == nil { + return + } + log.Debugf(log.WebsocketMgr, + "New Order: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f", + o.Exchange, + o.AssetType, + o.Pair, + o.Status, + o.Type, + o.Side, + o.ID, + o.ClientOrderID, + o.Price, + o.Amount, + o.ExecutedAmount, + o.RemainingAmount) +} + +// printAccountHoldingsChangeSummary this function will be deprecated when a +// account holdings update is done. +func (m *websocketRoutineManager) printAccountHoldingsChangeSummary(o account.Change) { + if m == nil || atomic.LoadInt32(&m.started) == 0 { + return + } + log.Debugf(log.WebsocketMgr, + "Account Holdings Balance Changed: %s %s %s has changed balance by %f for account: %s", + o.Exchange, + o.Asset, + o.Currency, + o.Amount, + o.Account) +} diff --git a/engine/websocketroutine_manager.md b/engine/websocketroutine_manager.md new file mode 100644 index 00000000..516d93f1 --- /dev/null +++ b/engine/websocketroutine_manager.md @@ -0,0 +1,48 @@ +# GoCryptoTrader package Websocketroutine_manager + + + + +[![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/websocketroutine_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This websocketroutine_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Websocketroutine_manager ++ The websocket routine manager subsystem is used process websocket data in a unified manner across enabled exchanges with websocket support ++ It can help process orders to the order manager subsystem when it receives new data ++ Logs output of ticker and orderbook updates ++ The websocket routine manager subsystem can be enabled or disabled via runtime command `-websocketroutine=false` defaulting to true ++ Logs can be customised to display values the config value `fiatDisplayCurrency` under `currencyConfig` + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/websocketroutine_manager_test.go b/engine/websocketroutine_manager_test.go new file mode 100644 index 00000000..1f2411cf --- /dev/null +++ b/engine/websocketroutine_manager_test.go @@ -0,0 +1,260 @@ +package engine + +import ( + "errors" + "sync" + "testing" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" +) + +func TestWebsocketRoutineManagerSetup(t *testing.T) { + _, err := setupWebsocketRoutineManager(nil, nil, nil, nil, false) + if !errors.Is(err, errNilExchangeManager) { + t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) + } + + _, err = setupWebsocketRoutineManager(SetupExchangeManager(), nil, nil, nil, false) + if !errors.Is(err, errNilOrderManager) { + t.Errorf("error '%v', expected '%v'", err, errNilOrderManager) + } + + _, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, nil, nil, false) + if !errors.Is(err, errNilCurrencyPairSyncer) { + t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairSyncer) + } + _, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, nil, false) + if !errors.Is(err, errNilCurrencyConfig) { + t.Errorf("error '%v', expected '%v'", err, errNilCurrencyConfig) + } + + _, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, true) + if !errors.Is(err, errNilCurrencyPairFormat) { + t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairFormat) + } + + m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m == nil { + t.Error("expecting manager") + } +} + +func TestWebsocketRoutineManagerStart(t *testing.T) { + var m *websocketRoutineManager + err := m.Start() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + cfg := &config.CurrencyConfig{CurrencyPairFormat: &config.CurrencyPairFormatConfig{ + Uppercase: false, + Delimiter: "-", + }} + m, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, cfg, true) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, ErrSubSystemAlreadyStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted) + } +} + +func TestWebsocketRoutineManagerIsRunning(t *testing.T) { + var m *websocketRoutineManager + if m.IsRunning() { + t.Error("expected false") + } + + m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if m.IsRunning() { + t.Error("expected false") + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + if !m.IsRunning() { + t.Error("expected true") + } +} + +func TestWebsocketRoutineManagerStop(t *testing.T) { + var m *websocketRoutineManager + err := m.Stop() + if !errors.Is(err, ErrNilSubsystem) { + t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) + } + + m, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, &config.CurrencyConfig{}, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, ErrSubSystemNotStarted) { + t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted) + } + + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Stop() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } +} + +func TestWebsocketRoutineManagerHandleData(t *testing.T) { + var exchName = "Bitstamp" + var wg sync.WaitGroup + em := SetupExchangeManager() + exch, err := em.NewExchangeByName(exchName) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + exch.SetDefaults() + em.Add(exch) + + om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = om.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + cfg := &config.CurrencyConfig{CurrencyPairFormat: &config.CurrencyPairFormatConfig{ + Uppercase: false, + Delimiter: "-", + }} + m, err := setupWebsocketRoutineManager(em, om, &syncManager{}, cfg, true) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.Start() + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + var orderID = "1337" + err = m.WebsocketDataHandler(exchName, errors.New("error")) + if err == nil { + t.Error("Error not handled correctly") + } + err = m.WebsocketDataHandler(exchName, nil) + if err == nil { + t.Error("Expected nil data error") + } + err = m.WebsocketDataHandler(exchName, stream.FundingData{}) + if err != nil { + t.Error(err) + } + err = m.WebsocketDataHandler(exchName, &ticker.Price{ + ExchangeName: exchName, + Pair: currency.NewPair(currency.BTC, currency.USDC), + AssetType: asset.Spot, + }) + if !errors.Is(err, nil) { + t.Errorf("error '%v', expected '%v'", err, nil) + } + err = m.WebsocketDataHandler(exchName, stream.KlineData{}) + if err != nil { + t.Error(err) + } + origOrder := &order.Detail{ + Exchange: exchName, + ID: orderID, + Amount: 1337, + Price: 1337, + } + err = m.WebsocketDataHandler(exchName, origOrder) + if err != nil { + t.Error(err) + } + // Send it again since it exists now + err = m.WebsocketDataHandler(exchName, &order.Detail{ + Exchange: exchName, + ID: orderID, + Amount: 1338, + }) + if err != nil { + t.Error(err) + } + updated, err := m.orderManager.GetByExchangeAndID(origOrder.Exchange, origOrder.ID) + if err != nil { + t.Error(err) + } + if updated.Amount != 1338 { + t.Error("Bad pipeline") + } + + err = m.WebsocketDataHandler(exchName, &order.Modify{ + Exchange: "Bitstamp", + ID: orderID, + Status: order.Active, + }) + if err != nil { + t.Error(err) + } + updated, err = m.orderManager.GetByExchangeAndID(origOrder.Exchange, origOrder.ID) + if err != nil { + t.Error(err) + } + if updated.Status != order.Active { + t.Error("Expected order to be modified to Active") + } + + // Send some gibberish + err = m.WebsocketDataHandler(exchName, order.Stop) + if err != nil { + t.Error(err) + } + + err = m.WebsocketDataHandler(exchName, stream.UnhandledMessageWarning{ + Message: "there's an issue here's a tissue"}, + ) + if err != nil { + t.Error(err) + } + + classificationError := order.ClassificationError{ + Exchange: "test", + OrderID: "one", + Err: errors.New("lol"), + } + err = m.WebsocketDataHandler(exchName, classificationError) + if err == nil { + t.Error("Expected error") + } + if !errors.Is(err, classificationError.Err) { + t.Errorf("error '%v', expected '%v'", err, classificationError.Err) + } + + err = m.WebsocketDataHandler(exchName, &orderbook.Base{ + Exchange: "Bitstamp", + Pair: currency.NewPair(currency.BTC, currency.USD), + }) + if err != nil { + t.Error(err) + } + err = m.WebsocketDataHandler(exchName, "this is a test string") + if err != nil { + t.Error(err) + } +} diff --git a/engine/websocketroutine_manager_types.go b/engine/websocketroutine_manager_types.go new file mode 100644 index 00000000..33acb92a --- /dev/null +++ b/engine/websocketroutine_manager_types.go @@ -0,0 +1,27 @@ +package engine + +import ( + "errors" + "sync" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +// websocketRoutineManager is used to process websocket updates from a unified location +type websocketRoutineManager struct { + started int32 + verbose bool + exchangeManager iExchangeManager + orderManager iOrderManager + syncer iCurrencyPairSyncer + currencyConfig *config.CurrencyConfig + shutdown chan struct{} + wg sync.WaitGroup +} + +var ( + errNilOrderManager = errors.New("nil order manager received") + errNilCurrencyPairSyncer = errors.New("nil currency pair syncer received") + errNilCurrencyConfig = errors.New("nil currency config received") + errNilCurrencyPairFormat = errors.New("nil currency pair format received") +) diff --git a/engine/withdraw.go b/engine/withdraw.go deleted file mode 100644 index c15b12b0..00000000 --- a/engine/withdraw.go +++ /dev/null @@ -1,245 +0,0 @@ -package engine - -import ( - "fmt" - "time" - - withdrawDataStore "github.com/thrasher-corp/gocryptotrader/database/repository/withdraw" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/gctrpc" - "github.com/thrasher-corp/gocryptotrader/log" - "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" - "google.golang.org/protobuf/types/known/timestamppb" -) - -const ( - // ErrWithdrawRequestNotFound message to display when no record is found - ErrWithdrawRequestNotFound = "%v not found" - // ErrRequestCannotbeNil message to display when request is nil - ErrRequestCannotbeNil = "request cannot be nil" - // StatusError const for for "error" string - StatusError = "error" -) - -// SubmitWithdrawal performs validation and submits a new withdraw request to -// exchange -func (bot *Engine) SubmitWithdrawal(req *withdraw.Request) (*withdraw.Response, error) { - if req == nil { - return nil, withdraw.ErrRequestCannotBeNil - } - - exch := bot.GetExchangeByName(req.Exchange) - if exch == nil { - return nil, ErrExchangeNotFound - } - - resp := &withdraw.Response{ - Exchange: withdraw.ExchangeResponse{ - Name: req.Exchange, - }, - RequestDetails: *req, - } - - var err error - if bot.Settings.EnableDryRun { - log.Warnln(log.Global, "Dry run enabled, no withdrawal request will be submitted or have an event created") - resp.ID = withdraw.DryRunID - resp.Exchange.Status = "dryrun" - resp.Exchange.ID = withdraw.DryRunID.String() - } else { - var ret *withdraw.ExchangeResponse - if req.Type == withdraw.Fiat { - ret, err = exch.WithdrawFiatFunds(req) - if err != nil { - resp.Exchange.ID = StatusError - resp.Exchange.Status = err.Error() - } else { - resp.Exchange.Status = ret.Status - resp.Exchange.ID = ret.ID - } - } else if req.Type == withdraw.Crypto { - ret, err = exch.WithdrawCryptocurrencyFunds(req) - if err != nil { - resp.Exchange.ID = StatusError - resp.Exchange.Status = err.Error() - } else { - resp.Exchange.Status = ret.Status - resp.Exchange.ID = ret.ID - } - } - withdrawDataStore.Event(resp) - } - if err == nil { - withdraw.Cache.Add(resp.ID, resp) - } - return resp, nil -} - -// WithdrawalEventByID returns a withdrawal request by ID -func WithdrawalEventByID(id string) (*withdraw.Response, error) { - v := withdraw.Cache.Get(id) - if v != nil { - return v.(*withdraw.Response), nil - } - - l, err := withdrawDataStore.GetEventByUUID(id) - if err != nil { - return nil, fmt.Errorf(ErrWithdrawRequestNotFound, id) - } - withdraw.Cache.Add(id, l) - return l, nil -} - -// WithdrawalEventByExchange returns a withdrawal request by ID -func WithdrawalEventByExchange(exchange string, limit int) ([]*withdraw.Response, error) { - return withdrawDataStore.GetEventsByExchange(exchange, limit) -} - -// WithdrawEventByDate returns a withdrawal request by ID -func WithdrawEventByDate(exchange string, start, end time.Time, limit int) ([]*withdraw.Response, error) { - return withdrawDataStore.GetEventsByDate(exchange, start, end, limit) -} - -// WithdrawalEventByExchangeID returns a withdrawal request by Exchange ID -func WithdrawalEventByExchangeID(exchange, id string) (*withdraw.Response, error) { - return withdrawDataStore.GetEventByExchangeID(exchange, id) -} - -func parseMultipleEvents(ret []*withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse { - v := &gctrpc.WithdrawalEventsByExchangeResponse{} - for x := range ret { - tempEvent := &gctrpc.WithdrawalEventResponse{ - Id: ret[x].ID.String(), - Exchange: &gctrpc.WithdrawlExchangeEvent{ - Name: ret[x].Exchange.Name, - Id: ret[x].Exchange.ID, - Status: ret[x].Exchange.Status, - }, - Request: &gctrpc.WithdrawalRequestEvent{ - Currency: ret[x].RequestDetails.Currency.String(), - Description: ret[x].RequestDetails.Description, - Amount: ret[x].RequestDetails.Amount, - Type: int32(ret[x].RequestDetails.Type), - }, - } - - tempEvent.CreatedAt = timestamppb.New(ret[x].CreatedAt) - if err := tempEvent.CreatedAt.CheckValid(); err != nil { - log.Errorf(log.Global, "withdrawal parseMultipleEvents CreatedAt: %s", err) - } - tempEvent.UpdatedAt = timestamppb.New(ret[x].UpdatedAt) - if err := tempEvent.UpdatedAt.CheckValid(); err != nil { - log.Errorf(log.Global, "withdrawal parseMultipleEvents UpdatedAt: %s", err) - } - - if ret[x].RequestDetails.Type == withdraw.Crypto { - tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) - tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ - Address: ret[x].RequestDetails.Crypto.Address, - AddressTag: ret[x].RequestDetails.Crypto.AddressTag, - Fee: ret[x].RequestDetails.Crypto.FeeAmount, - } - } else if ret[x].RequestDetails.Type == withdraw.Fiat { - if ret[x].RequestDetails.Fiat != (withdraw.FiatRequest{}) { - tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) - tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ - BankName: ret[x].RequestDetails.Fiat.Bank.BankName, - AccountName: ret[x].RequestDetails.Fiat.Bank.AccountName, - AccountNumber: ret[x].RequestDetails.Fiat.Bank.AccountNumber, - Bsb: ret[x].RequestDetails.Fiat.Bank.BSBNumber, - Swift: ret[x].RequestDetails.Fiat.Bank.SWIFTCode, - Iban: ret[x].RequestDetails.Fiat.Bank.IBAN, - } - } - } - v.Event = append(v.Event, tempEvent) - } - return v -} - -func parseWithdrawalsHistory(ret []exchange.WithdrawalHistory, exchName string, limit int) *gctrpc.WithdrawalEventsByExchangeResponse { - v := &gctrpc.WithdrawalEventsByExchangeResponse{} - for x := range ret { - if limit > 0 && x >= limit { - return v - } - - tempEvent := &gctrpc.WithdrawalEventResponse{ - Id: ret[x].TransferID, - Exchange: &gctrpc.WithdrawlExchangeEvent{ - Name: exchName, - Status: ret[x].Status, - }, - Request: &gctrpc.WithdrawalRequestEvent{ - Currency: ret[x].Currency, - Description: ret[x].Description, - Amount: ret[x].Amount, - }, - } - - tempEvent.UpdatedAt = timestamppb.New(ret[x].Timestamp) - if err := tempEvent.UpdatedAt.CheckValid(); err != nil { - log.Errorf(log.Global, "withdrawal parseWithdrawalsHistory UpdatedAt: %s", err) - } - - tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ - Address: ret[x].CryptoToAddress, - Fee: ret[x].Fee, - TxId: ret[x].CryptoTxID, - } - - v.Event = append(v.Event, tempEvent) - } - return v -} - -func parseSingleEvents(ret *withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse { - tempEvent := &gctrpc.WithdrawalEventResponse{ - Id: ret.ID.String(), - Exchange: &gctrpc.WithdrawlExchangeEvent{ - Name: ret.Exchange.Name, - Id: ret.Exchange.Name, - Status: ret.Exchange.Status, - }, - Request: &gctrpc.WithdrawalRequestEvent{ - Currency: ret.RequestDetails.Currency.String(), - Description: ret.RequestDetails.Description, - Amount: ret.RequestDetails.Amount, - Type: int32(ret.RequestDetails.Type), - }, - } - - tempEvent.CreatedAt = timestamppb.New(ret.CreatedAt) - if err := tempEvent.CreatedAt.CheckValid(); err != nil { - log.Errorf(log.Global, "withdrawal parseSingleEvents CreatedAt %s", err) - } - tempEvent.UpdatedAt = timestamppb.New(ret.UpdatedAt) - if err := tempEvent.UpdatedAt.CheckValid(); err != nil { - log.Errorf(log.Global, "withdrawal parseSingleEvents UpdatedAt: %s", err) - } - - if ret.RequestDetails.Type == withdraw.Crypto { - tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) - tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ - Address: ret.RequestDetails.Crypto.Address, - AddressTag: ret.RequestDetails.Crypto.AddressTag, - Fee: ret.RequestDetails.Crypto.FeeAmount, - } - } else if ret.RequestDetails.Type == withdraw.Fiat { - if ret.RequestDetails.Fiat != (withdraw.FiatRequest{}) { - tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) - tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ - BankName: ret.RequestDetails.Fiat.Bank.BankName, - AccountName: ret.RequestDetails.Fiat.Bank.AccountName, - AccountNumber: ret.RequestDetails.Fiat.Bank.AccountNumber, - Bsb: ret.RequestDetails.Fiat.Bank.BSBNumber, - Swift: ret.RequestDetails.Fiat.Bank.SWIFTCode, - Iban: ret.RequestDetails.Fiat.Bank.IBAN, - } - } - } - - return &gctrpc.WithdrawalEventsByExchangeResponse{ - Event: []*gctrpc.WithdrawalEventResponse{tempEvent}, - } -} diff --git a/engine/withdraw_manager.go b/engine/withdraw_manager.go new file mode 100644 index 00000000..c418f743 --- /dev/null +++ b/engine/withdraw_manager.go @@ -0,0 +1,143 @@ +package engine + +import ( + "errors" + "fmt" + "time" + + dbwithdraw "github.com/thrasher-corp/gocryptotrader/database/repository/withdraw" + "github.com/thrasher-corp/gocryptotrader/log" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +// SetupWithdrawManager creates a new withdraw manager +func SetupWithdrawManager(em iExchangeManager, pm iPortfolioManager, isDryRun bool) (*WithdrawManager, error) { + if em == nil { + return nil, errors.New("nil manager") + } + return &WithdrawManager{ + exchangeManager: em, + portfolioManager: pm, + isDryRun: isDryRun, + }, nil +} + +// SubmitWithdrawal performs validation and submits a new withdraw request to +// exchange +func (m *WithdrawManager) SubmitWithdrawal(req *withdraw.Request) (*withdraw.Response, error) { + if m == nil { + return nil, ErrNilSubsystem + } + if req == nil { + return nil, withdraw.ErrRequestCannotBeNil + } + + exch := m.exchangeManager.GetExchangeByName(req.Exchange) + if exch == nil { + return nil, ErrExchangeNotFound + } + + resp := &withdraw.Response{ + Exchange: withdraw.ExchangeResponse{ + Name: req.Exchange, + }, + RequestDetails: *req, + } + + var err error + if m.isDryRun { + log.Warnln(log.Global, "Dry run enabled, no withdrawal request will be submitted or have an event created") + resp.ID = withdraw.DryRunID + resp.Exchange.Status = "dryrun" + resp.Exchange.ID = withdraw.DryRunID.String() + } else { + var ret *withdraw.ExchangeResponse + if req.Type == withdraw.Crypto { + if !m.portfolioManager.IsWhiteListed(req.Crypto.Address) { + return nil, withdraw.ErrStrAddressNotWhiteListed + } + if !m.portfolioManager.IsExchangeSupported(req.Exchange, req.Crypto.Address) { + return nil, withdraw.ErrStrExchangeNotSupportedByAddress + } + } + if req.Type == withdraw.Fiat { + ret, err = exch.WithdrawFiatFunds(req) + if err != nil { + resp.Exchange.Status = err.Error() + } else { + resp.Exchange.Status = ret.Status + resp.Exchange.ID = ret.ID + } + } else if req.Type == withdraw.Crypto { + ret, err = exch.WithdrawCryptocurrencyFunds(req) + if err != nil { + resp.Exchange.Status = err.Error() + } else { + resp.Exchange.Status = ret.Status + resp.Exchange.ID = ret.ID + } + } + } + if err == nil { + withdraw.Cache.Add(resp.ID, resp) + } + dbwithdraw.Event(resp) + return resp, err +} + +// WithdrawalEventByID returns a withdrawal request by ID +func (m *WithdrawManager) WithdrawalEventByID(id string) (*withdraw.Response, error) { + if m == nil { + return nil, ErrNilSubsystem + } + v := withdraw.Cache.Get(id) + if v != nil { + return v.(*withdraw.Response), nil + } + + l, err := dbwithdraw.GetEventByUUID(id) + if err != nil { + return nil, fmt.Errorf("%w %v", ErrWithdrawRequestNotFound, id) + } + withdraw.Cache.Add(id, l) + return l, nil +} + +// WithdrawalEventByExchange returns a withdrawal request by ID +func (m *WithdrawManager) WithdrawalEventByExchange(exchange string, limit int) ([]*withdraw.Response, error) { + if m == nil { + return nil, ErrNilSubsystem + } + exch := m.exchangeManager.GetExchangeByName(exchange) + if exch == nil { + return nil, ErrExchangeNotFound + } + + return dbwithdraw.GetEventsByExchange(exchange, limit) +} + +// WithdrawEventByDate returns a withdrawal request by ID +func (m *WithdrawManager) WithdrawEventByDate(exchange string, start, end time.Time, limit int) ([]*withdraw.Response, error) { + if m == nil { + return nil, ErrNilSubsystem + } + exch := m.exchangeManager.GetExchangeByName(exchange) + if exch == nil { + return nil, ErrExchangeNotFound + } + + return dbwithdraw.GetEventsByDate(exchange, start, end, limit) +} + +// WithdrawalEventByExchangeID returns a withdrawal request by Exchange ID +func (m *WithdrawManager) WithdrawalEventByExchangeID(exchange, id string) (*withdraw.Response, error) { + if m == nil { + return nil, ErrNilSubsystem + } + exch := m.exchangeManager.GetExchangeByName(exchange) + if exch == nil { + return nil, ErrExchangeNotFound + } + + return dbwithdraw.GetEventByExchangeID(exchange, id) +} diff --git a/engine/withdraw_manager.md b/engine/withdraw_manager.md new file mode 100644 index 00000000..f4ebce69 --- /dev/null +++ b/engine/withdraw_manager.md @@ -0,0 +1,49 @@ +# GoCryptoTrader package Withdraw_manager + + + + +[![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/withdraw_manager) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This withdraw_manager 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 this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for Withdraw_manager ++ The withdraw manager subsystem is responsible for the processing of withdrawal requests and submitting them to exchanges ++ The withdraw manager can be interacted with via GRPC commands such as `WithdrawFiatRequest` and `WithdrawCryptoRequest` ++ Supports caching of responses to allow for quick viewing of withdrawal events via GRPC ++ If the database is enabled, withdrawal events are stored to the database for later viewing ++ Will not process withdrawal events if `dryrun` is true ++ The withdraw manager subsystem is always enabled + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/engine/withdraw_manager_test.go b/engine/withdraw_manager_test.go new file mode 100644 index 00000000..f057077e --- /dev/null +++ b/engine/withdraw_manager_test.go @@ -0,0 +1,187 @@ +package engine + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/binance" + "github.com/thrasher-corp/gocryptotrader/portfolio" + "github.com/thrasher-corp/gocryptotrader/portfolio/banking" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +const ( + exchangeName = "Binance" +) + +func withdrawManagerTestHelper(t *testing.T) (*ExchangeManager, *portfolioManager) { + t.Helper() + em := SetupExchangeManager() + b := new(binance.Binance) + b.SetDefaults() + em.Add(b) + pm, err := setupPortfolioManager(em, 0, &portfolio.Base{Addresses: []portfolio.Address{}}) + if err != nil { + t.Fatal(err) + } + + return em, pm +} + +func TestSubmitWithdrawal(t *testing.T) { + t.Parallel() + em, pm := withdrawManagerTestHelper(t) + m, err := SetupWithdrawManager(em, pm, false) + if err != nil { + t.Fatal(err) + } + + banking.Accounts = append(banking.Accounts, + banking.Account{ + Enabled: true, + ID: "test-bank-01", + BankName: "Test Bank", + BankAddress: "42 Bank Street", + BankPostalCode: "13337", + BankPostalCity: "Satoshiville", + BankCountry: "Japan", + AccountName: "Satoshi Nakamoto", + AccountNumber: "0234", + BSBNumber: "123456", + SWIFTCode: "91272837", + IBAN: "98218738671897", + SupportedCurrencies: "AUD,USD", + SupportedExchanges: "Binance", + }, + ) + bank, err := banking.GetBankAccountByID("test-bank-01") + if err != nil { + t.Error(err) + } + req := &withdraw.Request{ + Exchange: exchangeName, + Currency: currency.AUD, + Description: exchangeName, + Amount: 1.0, + Type: withdraw.Fiat, + Fiat: withdraw.FiatRequest{ + Bank: *bank, + }, + } + _, err = m.SubmitWithdrawal(req) + if !errors.Is(err, common.ErrFunctionNotSupported) { + t.Errorf("received %v, expected %v", err, common.ErrFunctionNotSupported) + } + + req.Type = withdraw.Crypto + req.Currency = currency.BTC + req.Crypto.Address = "1337" + _, err = m.SubmitWithdrawal(req) + if !errors.Is(err, withdraw.ErrStrAddressNotWhiteListed) { + t.Errorf("received %v, expected %v", err, withdraw.ErrStrAddressNotWhiteListed) + } + var wg sync.WaitGroup + err = pm.Start(&wg) + if err != nil { + t.Error(err) + } + err = pm.AddAddress("1337", "", req.Currency, 1337) + if err != nil { + t.Error(err) + } + adds := pm.GetAddresses() + adds[0].WhiteListed = true + if !errors.Is(err, nil) { + t.Errorf("received %v, expected %v", err, nil) + } + _, err = m.SubmitWithdrawal(req) + if !errors.Is(err, withdraw.ErrStrExchangeNotSupportedByAddress) { + t.Errorf("received %v, expected %v", err, withdraw.ErrStrExchangeNotSupportedByAddress) + } + + adds[0].SupportedExchanges = exchangeName + _, err = m.SubmitWithdrawal(req) + if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) { + t.Errorf("received %v, expected %v", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) + } + + _, err = m.SubmitWithdrawal(nil) + if !errors.Is(err, withdraw.ErrRequestCannotBeNil) { + t.Errorf("received %v, expected %v", err, withdraw.ErrRequestCannotBeNil) + } + + m.isDryRun = true + _, err = m.SubmitWithdrawal(req) + if !errors.Is(err, nil) { + t.Errorf("received %v, expected %v", err, nil) + } +} + +func TestWithdrawEventByID(t *testing.T) { + t.Parallel() + em, pm := withdrawManagerTestHelper(t) + m, err := SetupWithdrawManager(em, pm, false) + if err != nil { + t.Fatal(err) + } + tempResp := &withdraw.Response{ + ID: withdraw.DryRunID, + } + _, err = m.WithdrawalEventByID(withdraw.DryRunID.String()) + if !errors.Is(err, ErrWithdrawRequestNotFound) { + t.Errorf("received %v, expected %v", err, ErrWithdrawRequestNotFound) + } + + withdraw.Cache.Add(withdraw.DryRunID.String(), tempResp) + v, err := m.WithdrawalEventByID(withdraw.DryRunID.String()) + if !errors.Is(err, nil) { + t.Errorf("expected %v, received %v", nil, err) + } + if v == nil { + t.Error("expected WithdrawalEventByID() to return data from cache") + } +} + +func TestWithdrawalEventByExchange(t *testing.T) { + t.Parallel() + em, pm := withdrawManagerTestHelper(t) + m, err := SetupWithdrawManager(em, pm, false) + if err != nil { + t.Fatal(err) + } + _, err = m.WithdrawalEventByExchange(exchangeName, 1) + if err == nil { + t.Error(err) + } +} + +func TestWithdrawEventByDate(t *testing.T) { + t.Parallel() + em, pm := withdrawManagerTestHelper(t) + m, err := SetupWithdrawManager(em, pm, false) + if err != nil { + t.Fatal(err) + } + _, err = m.WithdrawEventByDate(exchangeName, time.Now(), time.Now(), 1) + if err == nil { + t.Error(err) + } +} + +func TestWithdrawalEventByExchangeID(t *testing.T) { + t.Parallel() + em, _ := withdrawManagerTestHelper(t) + m, err := SetupWithdrawManager(em, nil, false) + if err != nil { + t.Fatal(err) + } + _, err = m.WithdrawalEventByExchangeID(exchangeName, exchangeName) + if err == nil { + t.Error(err) + } +} diff --git a/engine/withdraw_manager_types.go b/engine/withdraw_manager_types.go new file mode 100644 index 00000000..6d0e1d2e --- /dev/null +++ b/engine/withdraw_manager_types.go @@ -0,0 +1,18 @@ +package engine + +import ( + "errors" +) + +var ( + // ErrWithdrawRequestNotFound message to display when no record is found + ErrWithdrawRequestNotFound = errors.New("request not found") +) + +// WithdrawManager is responsible for performing withdrawal requests and +// saving them to the database +type WithdrawManager struct { + exchangeManager iExchangeManager + portfolioManager iPortfolioManager + isDryRun bool +} diff --git a/engine/withdraw_test.go b/engine/withdraw_test.go deleted file mode 100644 index 27c618ab..00000000 --- a/engine/withdraw_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package engine - -import ( - "fmt" - "os" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/portfolio/banking" - "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" -) - -const ( - bankAccountID = "test-bank-01" -) - -var ( - settings = Settings{ - ConfigFile: filepath.Join("..", "testdata", "configtest.json"), - EnableDryRun: true, - DataDir: filepath.Join("..", "testdata", "gocryptotrader"), - Verbose: false, - EnableGRPC: false, - EnableDeprecatedRPC: false, - EnableWebsocketRPC: false, - } -) - -func cleanup() { - err := os.RemoveAll(settings.DataDir) - if err != nil { - fmt.Printf("Clean up failed to remove file: %v manual removal may be required", err) - } -} - -func TestSubmitWithdrawal(t *testing.T) { - bot := CreateTestBot(t) - if config.Cfg.Name == "" { - config.Cfg = *bot.Config - } - banking.Accounts = append(banking.Accounts, - banking.Account{ - Enabled: true, - ID: "test-bank-01", - BankName: "Test Bank", - BankAddress: "42 Bank Street", - BankPostalCode: "13337", - BankPostalCity: "Satoshiville", - BankCountry: "Japan", - AccountName: "Satoshi Nakamoto", - AccountNumber: "0234", - BSBNumber: "123456", - SWIFTCode: "91272837", - IBAN: "98218738671897", - SupportedCurrencies: "AUD,USD", - SupportedExchanges: testExchange, - }, - ) - - bank, err := banking.GetBankAccountByID(bankAccountID) - if err != nil { - t.Fatal(err) - } - req := &withdraw.Request{ - Exchange: testExchange, - Currency: currency.AUD, - Description: testExchange, - Amount: 1.0, - Type: 1, - Fiat: withdraw.FiatRequest{ - Bank: *bank, - }, - } - - _, err = bot.SubmitWithdrawal(req) - if err != nil { - t.Fatal(err) - } - - _, err = bot.SubmitWithdrawal(nil) - if err != nil { - if err.Error() != withdraw.ErrRequestCannotBeNil.Error() { - t.Fatal(err) - } - } - cleanup() -} - -func TestWithdrawEventByID(t *testing.T) { - tempResp := &withdraw.Response{ - ID: withdraw.DryRunID, - } - _, err := WithdrawalEventByID(withdraw.DryRunID.String()) - if err != nil { - if err.Error() != fmt.Errorf(ErrWithdrawRequestNotFound, withdraw.DryRunID.String()).Error() { - t.Fatal(err) - } - } - withdraw.Cache.Add(withdraw.DryRunID.String(), tempResp) - v, err := WithdrawalEventByID(withdraw.DryRunID.String()) - if err != nil { - if err != fmt.Errorf(ErrWithdrawRequestNotFound, withdraw.DryRunID.String()) { - t.Fatal(err) - } - } - if v == nil { - t.Fatal("expected WithdrawalEventByID() to return data from cache") - } -} - -func TestWithdrawalEventByExchange(t *testing.T) { - _, err := WithdrawalEventByExchange(testExchange, 1) - if err == nil { - t.Fatal(err) - } -} - -func TestWithdrawEventByDate(t *testing.T) { - _, err := WithdrawEventByDate(testExchange, time.Now(), time.Now(), 1) - if err == nil { - t.Fatal(err) - } -} - -func TestWithdrawalEventByExchangeID(t *testing.T) { - _, err := WithdrawalEventByExchangeID(testExchange, testExchange) - if err == nil { - t.Fatal(err) - } -} - -func TestParseEvents(t *testing.T) { - var testData []*withdraw.Response - for x := 0; x < 5; x++ { - test := fmt.Sprintf("test-%v", x) - resp := &withdraw.Response{ - ID: withdraw.DryRunID, - Exchange: withdraw.ExchangeResponse{ - Name: test, - ID: test, - Status: test, - }, - RequestDetails: withdraw.Request{ - Exchange: test, - Description: test, - Amount: 1.0, - }, - } - if x%2 == 0 { - resp.RequestDetails.Currency = currency.AUD - resp.RequestDetails.Type = 1 - resp.RequestDetails.Fiat = withdraw.FiatRequest{ - Bank: banking.Account{ - Enabled: false, - ID: fmt.Sprintf("test-%v", x), - BankName: fmt.Sprintf("test-%v-bank", x), - AccountName: "hello", - AccountNumber: fmt.Sprintf("test-%v", x), - BSBNumber: "123456", - SupportedCurrencies: "BTC-AUD", - SupportedExchanges: testExchange, - }, - } - } else { - resp.RequestDetails.Currency = currency.BTC - resp.RequestDetails.Type = 0 - resp.RequestDetails.Crypto.Address = test - resp.RequestDetails.Crypto.FeeAmount = 0 - resp.RequestDetails.Crypto.AddressTag = test - } - testData = append(testData, resp) - } - v := parseMultipleEvents(testData) - if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" { - t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse") - } - - v = parseSingleEvents(testData[0]) - if reflect.TypeOf(v).String() != "*gctrpc.WithdrawalEventsByExchangeResponse" { - t.Fatal("expected type to be *gctrpc.WithdrawalEventsByExchangeResponse") - } - - v = parseSingleEvents(testData[1]) - if v.Event[0].Request.Type != 0 { - t.Fatal("Expected second entry in slice to return a Request.Type of Crypto") - } -} diff --git a/exchanges/README.md b/exchanges/README.md index 195a75b1..271dc954 100644 --- a/exchanges/README.md +++ b/exchanges/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Exchanges - + [![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) diff --git a/exchanges/alphapoint/README.md b/exchanges/alphapoint/README.md index cdb196ae..2406c681 100644 --- a/exchanges/alphapoint/README.md +++ b/exchanges/alphapoint/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Alphapoint - + [![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) diff --git a/exchanges/alphapoint/alphapoint.go b/exchanges/alphapoint/alphapoint.go index 203a50f0..9c120a27 100644 --- a/exchanges/alphapoint/alphapoint.go +++ b/exchanges/alphapoint/alphapoint.go @@ -549,7 +549,7 @@ func (a *Alphapoint) SendHTTPRequest(ep exchange.URL, method, path string, data // SendAuthenticatedHTTPRequest sends an authenticated request func (a *Alphapoint) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path string, data map[string]interface{}, result interface{}) error { if !a.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, a.Name) + return fmt.Errorf("%s %w", a.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := a.API.Endpoints.GetURL(ep) diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 5e4f23ac..0fa3522b 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -328,23 +328,23 @@ func (a *Alphapoint) GetDepositAddress(cryptocurrency currency.Code, _ string) ( // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (a *Alphapoint) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (a *Alphapoint) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrNotYetImplemented } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted -func (a *Alphapoint) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (a *Alphapoint) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrNotYetImplemented } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (a *Alphapoint) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (string, error) { +func (a *Alphapoint) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (string, error) { return "", common.ErrNotYetImplemented } // GetFeeByType returns an estimate of fee based on type of transaction -func (a *Alphapoint) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { +func (a *Alphapoint) GetFeeByType(_ *exchange.FeeBuilder) (float64, error) { return 0, common.ErrFunctionNotSupported } diff --git a/exchanges/binance/README.md b/exchanges/binance/README.md index b0681c59..2f562827 100644 --- a/exchanges/binance/README.md +++ b/exchanges/binance/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Binance - + [![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) diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 6680dc88..91f898cc 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -709,7 +709,7 @@ func (b *Binance) SendAPIKeyHTTPRequest(ePath exchange.URL, path string, f reque // SendAuthHTTPRequest sends an authenticated HTTP request func (b *Binance) SendAuthHTTPRequest(ePath exchange.URL, method, path string, params url.Values, f request.EndpointLimit, result interface{}) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpointPath, err := b.API.Endpoints.GetURL(ePath) if err != nil { diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 965c79a5..7efbf55a 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -1324,33 +1324,29 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() && mockTests { // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0.1) || err != nil { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(100000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(100000), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.1) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.1), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } @@ -1358,16 +1354,14 @@ func TestGetFee(t *testing.T) { // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -1375,8 +1369,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -1384,8 +1377,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } @@ -1925,7 +1917,7 @@ func TestWithdraw(t *testing.T) { withdrawCryptoRequest := withdraw.Request{ Exchange: b.Name, - Amount: 0, + Amount: 0.00001337, Currency: currency.BTC, Description: "WITHDRAW IT ALL", Crypto: withdraw.CryptoRequest{ @@ -1939,8 +1931,8 @@ func TestWithdraw(t *testing.T) { t.Error("Withdraw() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Withdraw() expecting an error when no keys are set") - case mockTests && err == nil: - t.Error("Mock Withdraw() error cannot be nil") + case mockTests && err != nil: + t.Error(err) } } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index fbde7721..902511d5 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -1127,7 +1127,6 @@ func (b *Binance) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - amountStr := strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64) v, err := b.WithdrawCrypto(withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, @@ -1143,13 +1142,13 @@ func (b *Binance) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Binance) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Binance) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Binance) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Binance) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/bitfinex/README.md b/exchanges/bitfinex/README.md index f74db2ba..a8795104 100644 --- a/exchanges/bitfinex/README.md +++ b/exchanges/bitfinex/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Bitfinex - + [![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) diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index fa414401..05c39848 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -1391,8 +1391,7 @@ func (b *Bitfinex) SendHTTPRequest(ep exchange.URL, path string, result interfac // unmarshals result to a supplied variable func (b *Bitfinex) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path string, params map[string]interface{}, result interface{}, endpoint request.EndpointLimit) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := b.API.Endpoints.GetURL(ep) @@ -1444,8 +1443,7 @@ func (b *Bitfinex) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path st // unmarshals result to a supplied variable func (b *Bitfinex) SendAuthenticatedHTTPRequestV2(ep exchange.URL, method, path string, params map[string]interface{}, result interface{}, endpoint request.EndpointLimit) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := b.API.Endpoints.GetURL(ep) if err != nil { @@ -1510,7 +1508,7 @@ func (b *Bitfinex) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { if err != nil { return 0, err } - case exchange.CyptocurrencyDepositFee: + case exchange.CryptocurrencyDepositFee: //TODO: fee is charged when < $1000USD is transferred, need to infer value in some way fee = 0 case exchange.CryptocurrencyWithdrawalFee: diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index af6c9a52..be3614f3 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -711,50 +711,44 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0004) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0004), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -762,8 +756,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -771,8 +764,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index a8cb90cb..23d8918b 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -147,7 +147,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { b.Websocket.DataHandler <- d b.WsAddSubscriptionChannel(0, "account", "N/A") } else if status == "fail" { - return fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s", + return fmt.Errorf("websocket unable to AUTH. Error code: %s", d["code"].(string)) } } @@ -190,7 +190,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { chanInfo, ok := b.WebsocketSubdChannels[chanID] if !ok && chanID != 0 { - return fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d", + return fmt.Errorf("unable to locate chanID: %d", chanID) } @@ -282,7 +282,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { } err := b.WsInsertSnapshot(pair, chanAsset, newOrderbook, fundingRate) if err != nil { - return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", + return fmt.Errorf("inserting snapshot error: %s", err) } case float64: @@ -315,7 +315,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { err := b.WsUpdateOrderbook(pair, chanAsset, newOrderbook, chanID, int64(sequenceNo), fundingRate) if err != nil { - return fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s", + return fmt.Errorf("updating orderbook error: %s", err) } } @@ -935,7 +935,7 @@ func (b *Bitfinex) wsHandleOrder(data []interface{}) { // channel func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook, fundingRate bool) error { if len(books) == 0 { - return errors.New("bitfinex.go error - no orderbooks submitted") + return errors.New("no orderbooks submitted") } var book orderbook.Base for i := range books { diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 050f2949..f5663d01 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -730,7 +730,6 @@ func (b *Bitfinex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request if err := withdrawRequest.Validate(); err != nil { return nil, err } - // Bitfinex has support for three types, exchange, margin and deposit // As this is for trading, I've made the wrapper default 'exchange' // TODO: Discover an automated way to make the decision for wallet type to withdraw from @@ -756,7 +755,6 @@ func (b *Bitfinex) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdr if err := withdrawRequest.Validate(); err != nil { return nil, err } - withdrawalType := "wire" // Bitfinex has support for three types, exchange, margin and deposit // As this is for trading, I've made the wrapper default 'exchange' @@ -779,7 +777,6 @@ func (b *Bitfinex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdra if err := withdrawRequest.Validate(); err != nil { return nil, err } - v, err := b.WithdrawFiatFunds(withdrawRequest) if err != nil { return nil, err diff --git a/exchanges/bitflyer/README.md b/exchanges/bitflyer/README.md index bfc85f44..18ce2ad2 100644 --- a/exchanges/bitflyer/README.md +++ b/exchanges/bitflyer/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Bitflyer - + [![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) diff --git a/exchanges/bitflyer/bitflyer_test.go b/exchanges/bitflyer/bitflyer_test.go index 659c5934..14e76e0e 100644 --- a/exchanges/bitflyer/bitflyer_test.go +++ b/exchanges/bitflyer/bitflyer_test.go @@ -205,50 +205,44 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.1) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.1), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -256,8 +250,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.JPY - if resp, err := b.GetFee(feeBuilder); resp != float64(324) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(324), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -265,8 +258,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.JPY - if resp, err := b.GetFee(feeBuilder); resp != float64(540) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(540), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 8a375aaa..b3cccd36 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -383,7 +383,7 @@ func (b *Bitflyer) CancelOrder(_ *order.Cancel) error { } // CancelBatchOrders cancels an orders by their corresponding ID numbers -func (b *Bitflyer) CancelBatchOrders(o []order.Cancel) (order.CancelBatchResponse, error) { +func (b *Bitflyer) CancelBatchOrders(_ []order.Cancel) (order.CancelBatchResponse, error) { return order.CancelBatchResponse{}, common.ErrNotYetImplemented } @@ -395,42 +395,42 @@ func (b *Bitflyer) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, er } // GetOrderInfo returns order information based on order ID -func (b *Bitflyer) GetOrderInfo(orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) { +func (b *Bitflyer) GetOrderInfo(_ string, _ currency.Pair, _ asset.Item) (order.Detail, error) { var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } // GetDepositAddress returns a deposit address for a specified currency -func (b *Bitflyer) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { +func (b *Bitflyer) GetDepositAddress(_ currency.Code, _ string) (string, error) { return "", common.ErrNotYetImplemented } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitflyer) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bitflyer) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrNotYetImplemented } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Bitflyer) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bitflyer) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrNotYetImplemented } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Bitflyer) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bitflyer) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrNotYetImplemented } // GetActiveOrders retrieves any orders that are active/open -func (b *Bitflyer) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { +func (b *Bitflyer) GetActiveOrders(_ *order.GetOrdersRequest) ([]order.Detail, error) { return nil, common.ErrNotYetImplemented } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Bitflyer) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { +func (b *Bitflyer) GetOrderHistory(_ *order.GetOrdersRequest) ([]order.Detail, error) { return nil, common.ErrNotYetImplemented } diff --git a/exchanges/bithumb/README.md b/exchanges/bithumb/README.md index 4f693df6..be053245 100644 --- a/exchanges/bithumb/README.md +++ b/exchanges/bithumb/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Bithumb - + [![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) diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index a977082f..e89b839e 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -468,7 +468,7 @@ func (b *Bithumb) SendHTTPRequest(ep exchange.URL, path string, result interface // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to bithumb func (b *Bithumb) SendAuthenticatedHTTPRequest(ep exchange.URL, path string, params url.Values, result interface{}) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := b.API.Endpoints.GetURL(ep) if err != nil { @@ -536,7 +536,7 @@ func (b *Bithumb) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount) - case exchange.CyptocurrencyDepositFee: + case exchange.CryptocurrencyDepositFee: fee = getDepositFee(feeBuilder.Pair.Base, feeBuilder.Amount) case exchange.CryptocurrencyWithdrawalFee: fee = getWithdrawalFee(feeBuilder.Pair.Base) diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 7f71b3b7..61224713 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -266,49 +266,43 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(2500) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2500), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -316,8 +310,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -325,8 +318,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 8aa2944b..8028507e 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -524,7 +524,6 @@ func (b *Bithumb) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - v, err := b.WithdrawCrypto(withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, withdrawRequest.Currency.String(), @@ -544,7 +543,6 @@ func (b *Bithumb) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdra if err := withdrawRequest.Validate(); err != nil { return nil, err } - if math.Mod(withdrawRequest.Amount, 1) != 0 { return nil, errors.New("currency KRW does not support decimal places") } @@ -567,7 +565,7 @@ func (b *Bithumb) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdra } // WithdrawFiatFundsToInternationalBank is not supported as Bithumb only withdraws KRW to South Korean banks -func (b *Bithumb) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bithumb) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/bitmex/README.md b/exchanges/bitmex/README.md index 91b4bf28..73f40772 100644 --- a/exchanges/bitmex/README.md +++ b/exchanges/bitmex/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Bitmex - + [![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) diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 0a707d11..70a81ee1 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -847,8 +847,7 @@ func (b *Bitmex) SendHTTPRequest(ep exchange.URL, path string, params Parameter, // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to bitmex func (b *Bitmex) SendAuthenticatedHTTPRequest(ep exchange.URL, verb, path string, params Parameter, result interface{}) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := b.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index d4868d8c..d8282345 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -474,49 +474,43 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0.00075) || err != nil { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.00075), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(750) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(750), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -524,8 +518,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -533,8 +526,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 0e6c9255..198ace92 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -486,7 +486,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error { // ProcessOrderbook processes orderbook updates func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.Pair, a asset.Item) error { if len(data) < 1 { - return errors.New("bitmex_websocket.go error - no orderbook data") + return errors.New("no orderbook data") } switch action { @@ -516,7 +516,7 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency. err := b.Websocket.Orderbook.LoadSnapshot(&book) if err != nil { - return fmt.Errorf("bitmex_websocket.go process orderbook error - %s", + return fmt.Errorf("process orderbook error - %s", err) } default: diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index efa2a58f..54de796f 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -652,18 +652,17 @@ func (b *Bitmex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - - var request = UserRequestWithdrawalParams{ + var r = UserRequestWithdrawalParams{ Address: withdrawRequest.Crypto.Address, Amount: withdrawRequest.Amount, Currency: withdrawRequest.Currency.String(), OtpToken: withdrawRequest.OneTimePassword, } if withdrawRequest.Crypto.FeeAmount > 0 { - request.Fee = withdrawRequest.Crypto.FeeAmount + r.Fee = withdrawRequest.Crypto.FeeAmount } - resp, err := b.UserRequestWithdrawal(request) + resp, err := b.UserRequestWithdrawal(r) if err != nil { return nil, err } @@ -676,13 +675,13 @@ func (b *Bitmex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitmex) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bitmex) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitmex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bitmex) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/bitstamp/README.md b/exchanges/bitstamp/README.md index 3949f882..c67e749f 100644 --- a/exchanges/bitstamp/README.md +++ b/exchanges/bitstamp/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Bitstamp - + [![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) diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index 9f80dc8c..6b0a1eeb 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -80,7 +80,7 @@ func (b *Bitstamp) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { feeBuilder.PurchasePrice, feeBuilder.Amount, balance) - case exchange.CyptocurrencyDepositFee: + case exchange.CryptocurrencyDepositFee: fee = 0 case exchange.InternationalBankDepositFee: fee = getInternationalBankDepositFee(feeBuilder.Amount) @@ -611,7 +611,7 @@ func (b *Bitstamp) SendHTTPRequest(ep exchange.URL, path string, result interfac // SendAuthenticatedHTTPRequest sends an authenticated request func (b *Bitstamp) SendAuthenticatedHTTPRequest(ep exchange.URL, path string, v2 bool, values url.Values, result interface{}) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := b.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index ee92b596..716cd7c3 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -65,61 +65,43 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -127,10 +109,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(7.5) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(7.5), - resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -138,10 +117,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(15) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(15), - resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index 6f7032a2..3b098aca 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -208,7 +208,7 @@ func (b *Bitstamp) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscriptio func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, assetType asset.Item) error { if len(update.Asks) == 0 && len(update.Bids) == 0 { - return errors.New("bitstamp_websocket.go error - no orderbook data") + return errors.New("no orderbook data") } var asks, bids []orderbook.Item for i := range update.Asks { diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index bfc6f1f7..7d8f3937 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -542,7 +542,6 @@ func (b *Bitstamp) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := b.CryptoWithdrawal(withdrawRequest.Amount, withdrawRequest.Crypto.Address, withdrawRequest.Currency.String(), @@ -570,7 +569,6 @@ func (b *Bitstamp) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdr if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := b.OpenBankWithdrawal(withdrawRequest.Amount, withdrawRequest.Currency.String(), withdrawRequest.Fiat.Bank.AccountName, @@ -605,7 +603,6 @@ func (b *Bitstamp) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdra if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := b.OpenInternationalBankWithdrawal(withdrawRequest.Amount, withdrawRequest.Currency.String(), withdrawRequest.Fiat.Bank.AccountName, diff --git a/exchanges/bittrex/README.md b/exchanges/bittrex/README.md index 5f89bb21..a52c48bf 100644 --- a/exchanges/bittrex/README.md +++ b/exchanges/bittrex/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Bittrex - + [![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) diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index bff66776..358ffe64 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -362,7 +362,7 @@ func (b *Bittrex) SendHTTPRequest(ep exchange.URL, path string, result interface // SendAuthHTTPRequest sends an authenticated request func (b *Bittrex) SendAuthHTTPRequest(ep exchange.URL, method, action string, params url.Values, data, result interface{}, resultHeader *http.Header) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := b.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 24e81578..e77c289f 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -354,49 +354,43 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("Expected: %f, Received: %f", float64(0.0025), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(2500) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(2500), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0.0025), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0003) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0.0003), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -404,8 +398,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -413,8 +406,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index e448a64e..1853100b 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -702,7 +702,6 @@ func (b *Bittrex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - result, err := b.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.Crypto.AddressTag, withdrawRequest.Crypto.Address, @@ -719,13 +718,13 @@ func (b *Bittrex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Bittrex) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bittrex) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Bittrex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *Bittrex) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/btcmarkets/README.md b/exchanges/btcmarkets/README.md index ad1469c3..f7f143e5 100644 --- a/exchanges/btcmarkets/README.md +++ b/exchanges/btcmarkets/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Btcmarkets - + [![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) diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index e108befb..07f32b6e 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -696,8 +696,7 @@ func (b *BTCMarkets) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedRequest sends an authenticated HTTP request func (b *BTCMarkets) SendAuthenticatedRequest(method, path string, data, result interface{}, f request.EndpointLimit) (err error) { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } now := time.Now() diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index bcd04d46..7a36c266 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -673,7 +673,6 @@ func (b *BTCMarkets) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*with if err := withdrawRequest.Validate(); err != nil { return nil, err } - if withdrawRequest.Currency != currency.AUD { return nil, errors.New("only aud is supported for withdrawals") } @@ -695,7 +694,7 @@ func (b *BTCMarkets) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*with // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *BTCMarkets) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *BTCMarkets) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/btse/README.md b/exchanges/btse/README.md index bd269271..1312f295 100644 --- a/exchanges/btse/README.md +++ b/exchanges/btse/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Btse - + [![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) diff --git a/exchanges/btse/btse.go b/exchanges/btse/btse.go index dcf9cbff..e6ecd94d 100644 --- a/exchanges/btse/btse.go +++ b/exchanges/btse/btse.go @@ -452,8 +452,7 @@ func (b *BTSE) SendHTTPRequest(ep exchange.URL, method, endpoint string, result // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the desired endpoint func (b *BTSE) SendAuthenticatedHTTPRequest(ep exchange.URL, method, endpoint string, isSpot bool, values url.Values, req map[string]interface{}, result interface{}, f request.EndpointLimit) error { if !b.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - b.Name) + return fmt.Errorf("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := b.API.Endpoints.GetURL(ep) diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 1d9bf6ac..e6cd2ec8 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -475,50 +475,42 @@ func TestGetFee(t *testing.T) { PurchasePrice: 1000, } - if resp, err := b.GetFee(feeBuilder); resp != 1.000000 || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", 1.000000, resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.IsMaker = false - if resp, err := b.GetFee(feeBuilder); resp != 2.00000 || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", 2.00000, resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != 0.0005 || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", 0.0005, resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.Pair.Base = currency.USDT - if resp, err := b.GetFee(feeBuilder); resp != 1.080000 || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", 1.080000, resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(3) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.Amount = 1000000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(900) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(900), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } feeBuilder.Amount = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(25) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(25), resp) + if _, err := b.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index dd8f7945..a6175f5e 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -699,7 +699,6 @@ func (b *BTSE) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (* if err := withdrawRequest.Validate(); err != nil { return nil, err } - amountToString := strconv.FormatFloat(withdrawRequest.Amount, 'f', 8, 64) resp, err := b.WalletWithdrawal(withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, @@ -716,13 +715,13 @@ func (b *BTSE) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (* // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *BTSE) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *BTSE) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (b *BTSE) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (b *BTSE) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/coinbasepro/README.md b/exchanges/coinbasepro/README.md index 56004fc0..5158646f 100644 --- a/exchanges/coinbasepro/README.md +++ b/exchanges/coinbasepro/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Coinbasepro - + [![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) diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index 5a2bf5e2..301ae555 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -699,8 +699,7 @@ func (c *CoinbasePro) SendHTTPRequest(ep exchange.URL, path string, result inter // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path string, params map[string]interface{}, result interface{}) (err error) { if !c.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - c.Name) + return fmt.Errorf("%s %w", c.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := c.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index acd01ab4..365e851f 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -231,33 +231,29 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := c.GetFee(feeBuilder); resp != float64(0.003) || err != nil { + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := c.GetFee(feeBuilder); resp != float64(3000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3000), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.01), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } } @@ -265,16 +261,14 @@ func TestGetFee(t *testing.T) { // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -282,8 +276,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.EUR - if resp, err := c.GetFee(feeBuilder); resp != float64(0.15) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -291,8 +284,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := c.GetFee(feeBuilder); resp != float64(25) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index 3b34ce76..c5542942 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -339,7 +339,7 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { } if len(asks) == 0 && len(bids) == 0 { - return errors.New("coinbasepro_websocket.go error - no data in websocket update") + return errors.New("no data in websocket update") } p, err := currency.NewPairFromString(update.ProductID) diff --git a/exchanges/coinbene/README.md b/exchanges/coinbene/README.md index d7a3fb37..a69e3651 100644 --- a/exchanges/coinbene/README.md +++ b/exchanges/coinbene/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Coinbene - + [![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) diff --git a/exchanges/coinbene/coinbene.go b/exchanges/coinbene/coinbene.go index b2985be4..f46a80ac 100644 --- a/exchanges/coinbene/coinbene.go +++ b/exchanges/coinbene/coinbene.go @@ -1118,8 +1118,7 @@ func (c *Coinbene) SendHTTPRequest(ep exchange.URL, path string, f request.Endpo func (c *Coinbene) SendAuthHTTPRequest(ep exchange.URL, method, path, epPath string, isSwap bool, params, result interface{}, f request.EndpointLimit) error { if !c.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - c.Name) + return fmt.Errorf("%s %w", c.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := c.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/coinbene/coinbene_wrapper.go b/exchanges/coinbene/coinbene_wrapper.go index 2685ec63..4ebcaaaa 100644 --- a/exchanges/coinbene/coinbene_wrapper.go +++ b/exchanges/coinbene/coinbene_wrapper.go @@ -666,25 +666,25 @@ func (c *Coinbene) GetOrderInfo(orderID string, pair currency.Pair, assetType as } // GetDepositAddress returns a deposit address for a specified currency -func (c *Coinbene) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { +func (c *Coinbene) GetDepositAddress(_ currency.Code, _ string) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *Coinbene) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (c *Coinbene) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *Coinbene) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (c *Coinbene) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (c *Coinbene) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (c *Coinbene) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/coinut/README.md b/exchanges/coinut/README.md index a3dabcd9..944a2508 100644 --- a/exchanges/coinut/README.md +++ b/exchanges/coinut/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Coinut - + [![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) diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index 3a4c1cf3..51ecfbef 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -263,7 +263,7 @@ func (c *COINUT) GetOpenPositions(instrumentID int) ([]OpenPosition, error) { // SendHTTPRequest sends either an authenticated or unauthenticated HTTP request func (c *COINUT) SendHTTPRequest(ep exchange.URL, apiRequest string, params map[string]interface{}, authenticated bool, result interface{}) (err error) { if !c.API.AuthenticatedSupport && authenticated { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, c.Name) + return fmt.Errorf("%s %w", c.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := c.API.Endpoints.GetURL(ep) diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 295673ae..76a53d79 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -133,49 +133,43 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := c.GetFee(feeBuilder); resp != float64(0.001) || err != nil { + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0010), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := c.GetFee(feeBuilder); resp != float64(1000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1000), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -183,8 +177,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.EUR - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -192,8 +185,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.USD - if resp, err := c.GetFee(feeBuilder); resp != float64(10) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(10), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -201,8 +193,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.SGD - if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -210,8 +201,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := c.GetFee(feeBuilder); resp != float64(10) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(10), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -219,8 +209,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.CAD - if resp, err := c.GetFee(feeBuilder); resp != float64(2) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -228,8 +217,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.SGD - if resp, err := c.GetFee(feeBuilder); resp != float64(10) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(10), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -237,8 +225,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.CAD - if resp, err := c.GetFee(feeBuilder); resp != float64(2) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2), resp) + if _, err := c.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index d310a7ab..065c7c07 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -782,30 +782,30 @@ func (c *COINUT) CancelAllOrders(details *order.Cancel) (order.CancelAllResponse } // GetOrderInfo returns order information based on order ID -func (c *COINUT) GetOrderInfo(orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) { +func (c *COINUT) GetOrderInfo(_ string, _ currency.Pair, _ asset.Item) (order.Detail, error) { return order.Detail{}, common.ErrNotYetImplemented } // GetDepositAddress returns a deposit address for a specified currency -func (c *COINUT) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { +func (c *COINUT) GetDepositAddress(_ currency.Code, _ string) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *COINUT) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (c *COINUT) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (c *COINUT) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (c *COINUT) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (c *COINUT) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (c *COINUT) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 969dd516..c04af054 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -27,8 +27,6 @@ import ( const ( warningBase64DecryptSecretKeyFailed = "exchange %s unable to base64 decode secret key.. Disabling Authenticated API support" // nolint // False positive (G101: Potential hardcoded credentials) - // WarningAuthenticatedRequestWithoutCredentialsSet error message for authenticated request without credentials set - WarningAuthenticatedRequestWithoutCredentialsSet = "exchange %s authenticated HTTP request called but not supported due to unset/default API keys" // DefaultHTTPTimeout is the default HTTP/HTTPS Timeout for exchange requests DefaultHTTPTimeout = time.Second * 15 // DefaultWebsocketResponseCheckTimeout is the default delay in checking for an expected websocket response @@ -39,6 +37,9 @@ const ( DefaultWebsocketOrderbookBufferLimit = 5 ) +// ErrAuthenticatedRequestWithoutCredentialsSet error message for authenticated request without credentials set +var ErrAuthenticatedRequestWithoutCredentialsSet = errors.New("authenticated HTTP request called but not supported due to unset/default API keys") + func (b *Base) checkAndInitRequester() { if b.Requester == nil { b.Requester = request.New(b.Name, @@ -90,7 +91,7 @@ func (b *Base) SetClientProxyAddress(addr string) error { } proxy, err := url.Parse(addr) if err != nil { - return fmt.Errorf("exchange.go - setting proxy address error %s", + return fmt.Errorf("setting proxy address error %s", err) } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 71ac4f91..6b03e203 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -477,7 +477,7 @@ func TestSetCurrencyPairFormat(t *testing.T) { } b.SetCurrencyPairFormat() if b.Config.CurrencyPairs == nil { - t.Error("CurrencyPairs shouldn't be nil") + t.Error("currencyPairs shouldn't be nil") } // Test global format logic diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index dda436da..76b0dacc 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -43,7 +43,7 @@ const ( InternationalBankDepositFee InternationalBankWithdrawalFee CryptocurrencyTradeFee - CyptocurrencyDepositFee + CryptocurrencyDepositFee CryptocurrencyWithdrawalFee OfflineTradeFee // Definitions for each type of withdrawal method for a given exchange diff --git a/exchanges/exmo/README.md b/exchanges/exmo/README.md index 3a92a06a..2de9d9e5 100644 --- a/exchanges/exmo/README.md +++ b/exchanges/exmo/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Exmo - + [![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) diff --git a/exchanges/exmo/exmo.go b/exchanges/exmo/exmo.go index 2767e8fe..277dd56d 100644 --- a/exchanges/exmo/exmo.go +++ b/exchanges/exmo/exmo.go @@ -318,8 +318,7 @@ func (e *EXMO) SendHTTPRequest(endpoint exchange.URL, path string, result interf // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (e *EXMO) SendAuthenticatedHTTPRequest(epath exchange.URL, method, endpoint string, vals url.Values, result interface{}) error { if !e.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - e.Name) + return fmt.Errorf("%s %w", e.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } urlPath, err := e.API.Endpoints.GetURL(epath) diff --git a/exchanges/exmo/exmo_test.go b/exchanges/exmo/exmo_test.go index c5396f6b..88e751f1 100644 --- a/exchanges/exmo/exmo_test.go +++ b/exchanges/exmo/exmo_test.go @@ -144,41 +144,36 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := e.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := e.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := e.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := e.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -186,16 +181,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -203,8 +196,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.RUB - if resp, err := e.GetFee(feeBuilder); resp != float64(1600) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1600), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -212,8 +204,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.PLN - if resp, err := e.GetFee(feeBuilder); resp != float64(30) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(30), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -221,8 +212,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.PLN - if resp, err := e.GetFee(feeBuilder); resp != float64(125) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(125), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -230,8 +220,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.TRY - if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -239,8 +228,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.EUR - if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -248,8 +236,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.RUB - if resp, err := e.GetFee(feeBuilder); resp != float64(3200) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3200), resp) + if _, err := e.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index df3088f4..a33c0a12 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -538,7 +538,6 @@ func (e *EXMO) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (* if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := e.WithdrawCryptocurrency(withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, @@ -551,13 +550,13 @@ func (e *EXMO) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (* // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (e *EXMO) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (e *EXMO) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (e *EXMO) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (e *EXMO) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/ftx/README.md b/exchanges/ftx/README.md index 694baa08..16cfde4e 100644 --- a/exchanges/ftx/README.md +++ b/exchanges/ftx/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Ftx - + [![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) diff --git a/exchanges/ftx/ftx.go b/exchanges/ftx/ftx.go index 42405568..bbe915df 100644 --- a/exchanges/ftx/ftx.go +++ b/exchanges/ftx/ftx.go @@ -1159,7 +1159,7 @@ func (f *FTX) UpdateSubaccountName(oldName, newName string) (*Subaccount, error) return &resp.Data, nil } -// DeleteSubaccountName deletes the specified subaccount name +// DeleteSubaccount deletes the specified subaccount name func (f *FTX) DeleteSubaccount(name string) error { if name == "" { return errSubaccountNameMustBeSpecified diff --git a/exchanges/ftx/ftx_test.go b/exchanges/ftx/ftx_test.go index 8c089ec7..dd0e9670 100644 --- a/exchanges/ftx/ftx_test.go +++ b/exchanges/ftx/ftx_test.go @@ -961,30 +961,20 @@ func TestGetFee(t *testing.T) { } x.IsMaker = false if areTestAPIKeysSet() { - a, err = f.GetFee(&x) - if err != nil { + if _, err = f.GetFee(&x); err != nil { t.Error(err) } - if a != 0.00865 { - t.Errorf("incorrect taker fee value") - } } x.FeeType = exchange.OfflineTradeFee - a, err = f.GetFee(&x) + _, err = f.GetFee(&x) if err != nil { t.Error(err) } - if a != 0.007 { - t.Errorf("incorrect offline taker fee value") - } x.IsMaker = true - a, err = f.GetFee(&x) + _, err = f.GetFee(&x) if err != nil { t.Error(err) } - if a != 0.002 { - t.Errorf("incorrect offline maker fee value") - } } func TestGetOfflineTradingFee(t *testing.T) { diff --git a/exchanges/ftx/ftx_wrapper.go b/exchanges/ftx/ftx_wrapper.go index 18c2e1f7..54b16029 100644 --- a/exchanges/ftx/ftx_wrapper.go +++ b/exchanges/ftx/ftx_wrapper.go @@ -749,7 +749,6 @@ func (f *FTX) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*w if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := f.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, diff --git a/exchanges/gateio/README.md b/exchanges/gateio/README.md index 40fe2c03..b9a4b43b 100644 --- a/exchanges/gateio/README.md +++ b/exchanges/gateio/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Gateio - + [![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) diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 33110389..69c5c48f 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -405,8 +405,7 @@ func (g *Gateio) GenerateSignature(message string) []byte { // To use this you must setup an APIKey and APISecret from the exchange func (g *Gateio) SendAuthenticatedHTTPRequest(ep exchange.URL, method, endpoint, param string, result interface{}) error { if !g.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - g.Name) + return fmt.Errorf("%s %w", g.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := g.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 7c205ce3..ab7e38a8 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -202,41 +202,36 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := g.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := g.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := g.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -244,24 +239,21 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -269,8 +261,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 3cfe0aaa..bac557c7 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -643,13 +643,13 @@ func (g *Gateio) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (g *Gateio) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (g *Gateio) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (g *Gateio) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (g *Gateio) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/gemini/README.md b/exchanges/gemini/README.md index 96c1f99f..5b2d6891 100644 --- a/exchanges/gemini/README.md +++ b/exchanges/gemini/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Gemini - + [![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) diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index e314b380..b4542d24 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -365,7 +365,7 @@ func (g *Gemini) SendHTTPRequest(ep exchange.URL, path string, result interface{ // exchange and returns an error func (g *Gemini) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path string, params map[string]interface{}, result interface{}) (err error) { if !g.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, g.Name) + return fmt.Errorf("%s %w", g.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := g.API.Endpoints.GetURL(ep) diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index a4ed98e4..2fd7e1c1 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -263,10 +263,7 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() || mockTests { // CryptocurrencyTradeFee Basic - if resp, err := g.GetFee(feeBuilder); resp != float64(0.0035) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0.0035), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -274,40 +271,28 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := g.GetFee(feeBuilder); resp != float64(3500) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(3500), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := g.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0.001), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -315,30 +300,21 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -346,10 +322,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), - resp) + if _, err := g.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 4139a4fd..dccd1015 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -613,7 +613,6 @@ func (g *Gemini) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := g.WithdrawCrypto(withdrawRequest.Crypto.Address, withdrawRequest.Currency.String(), withdrawRequest.Amount) if err != nil { return nil, err @@ -629,13 +628,13 @@ func (g *Gemini) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (g *Gemini) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (g *Gemini) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (g *Gemini) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (g *Gemini) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/hitbtc/README.md b/exchanges/hitbtc/README.md index 6bd56714..da67c56b 100644 --- a/exchanges/hitbtc/README.md +++ b/exchanges/hitbtc/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Hitbtc - + [![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) diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index f4ae21ca..2dceb6ba 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -539,8 +539,7 @@ func (h *HitBTC) SendHTTPRequest(ep exchange.URL, path string, result interface{ // SendAuthenticatedHTTPRequest sends an authenticated http request func (h *HitBTC) SendAuthenticatedHTTPRequest(ep exchange.URL, method, endpoint string, values url.Values, f request.EndpointLimit, result interface{}) error { if !h.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - h.Name) + return fmt.Errorf("%s %w", h.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := h.API.Endpoints.GetURL(ep) if err != nil { @@ -589,7 +588,7 @@ func (h *HitBTC) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { if err != nil { return 0, err } - case exchange.CyptocurrencyDepositFee: + case exchange.CryptocurrencyDepositFee: fee = calculateCryptocurrencyDepositFee(feeBuilder.Pair.Base, feeBuilder.Amount) case exchange.OfflineTradeFee: diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 433473f9..72a4fffe 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -179,69 +179,57 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := h.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0.042800) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.042800), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyWithdrawalFee Invalid currency feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err == nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee feeBuilder.Pair.Base = currency.BTC feeBuilder.Pair.Quote = currency.LTC - if resp, err := h.GetFee(feeBuilder); resp != float64(0.0006) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0006), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -249,8 +237,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index 37f57eba..82a26c7c 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -86,7 +86,7 @@ func (h *HitBTC) wsGetTableName(respRaw []byte) (string, error) { } } if init.Error.Message != "" || init.Error.Code != 0 { - return "", fmt.Errorf("hitbtc.go error - Code: %d, Message: %s", + return "", fmt.Errorf("code: %d, Message: %s", init.Error.Code, init.Error.Message) } @@ -292,7 +292,7 @@ func (h *HitBTC) wsHandleData(respRaw []byte) error { // WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error { if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 { - return errors.New("hitbtc.go error - no orderbooks to process") + return errors.New("no orderbooks to process") } var newOrderBook orderbook.Base diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index b1d79531..c79acab3 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -644,7 +644,6 @@ func (h *HitBTC) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - v, err := h.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Amount) if err != nil { return nil, err @@ -656,13 +655,13 @@ func (h *HitBTC) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (h *HitBTC) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (h *HitBTC) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (h *HitBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (h *HitBTC) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/huobi/README.md b/exchanges/huobi/README.md index 7c864ada..daa874a5 100644 --- a/exchanges/huobi/README.md +++ b/exchanges/huobi/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Huobi - + [![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) diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index 924b78c1..62607c62 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -818,7 +818,7 @@ func (h *HUOBI) SendHTTPRequest(ep exchange.URL, path string, result interface{} // SendAuthenticatedHTTPRequest sends authenticated requests to the HUOBI API func (h *HUOBI) SendAuthenticatedHTTPRequest(ep exchange.URL, method, endpoint string, values url.Values, data, result interface{}, isVersion2API bool) error { if !h.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name) + return fmt.Errorf("%s %w", h.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := h.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/huobi/huobi_futures.go b/exchanges/huobi/huobi_futures.go index c8c4aa86..d4282862 100644 --- a/exchanges/huobi/huobi_futures.go +++ b/exchanges/huobi/huobi_futures.go @@ -1109,7 +1109,7 @@ func (h *HUOBI) FQueryTriggerOrderHistory(contractCode currency.Pair, symbol, tr // FuturesAuthenticatedHTTPRequest sends authenticated requests to the HUOBI API func (h *HUOBI) FuturesAuthenticatedHTTPRequest(ep exchange.URL, method, endpoint string, values url.Values, data, result interface{}) error { if !h.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name) + return fmt.Errorf("%s %w", h.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := h.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 20bb9637..62c22f19 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -1824,74 +1824,59 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := h.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyWithdrawalFee Invalid currency feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := h.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 5dc9c78d..646e7723 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -1163,7 +1163,7 @@ func (h *HUOBI) GetOrderInfo(orderID string, pair currency.Pair, assetType asset } // GetDepositAddress returns a deposit address for a specified currency -func (h *HUOBI) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { +func (h *HUOBI) GetDepositAddress(cryptocurrency currency.Code, _ string) (string, error) { resp, err := h.QueryDepositAddress(cryptocurrency.Lower().String()) return resp.Address, err } @@ -1189,13 +1189,13 @@ func (h *HUOBI) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) ( // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (h *HUOBI) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (h *HUOBI) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (h *HUOBI) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (h *HUOBI) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/itbit/README.md b/exchanges/itbit/README.md index 5428edcf..6c79f984 100644 --- a/exchanges/itbit/README.md +++ b/exchanges/itbit/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Itbit - + [![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) diff --git a/exchanges/itbit/itbit.go b/exchanges/itbit/itbit.go index efe31f33..f8d1fc70 100644 --- a/exchanges/itbit/itbit.go +++ b/exchanges/itbit/itbit.go @@ -296,7 +296,7 @@ func (i *ItBit) SendHTTPRequest(ep exchange.URL, path string, result interface{} // SendAuthenticatedHTTPRequest sends an authenticated request to itBit func (i *ItBit) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path string, params map[string]interface{}, result interface{}) error { if !i.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, i.Name) + return fmt.Errorf("%s %w", i.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := i.API.Endpoints.GetURL(ep) if err != nil { @@ -366,7 +366,7 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path strin err = json.Unmarshal(intermediary, &errCheck) if err == nil { if errCheck.Code != 0 || errCheck.Description != "" { - return fmt.Errorf("itbit.go SendAuthRequest error code: %d description: %s", + return fmt.Errorf("sendAuthRequest error code: %d description: %s", errCheck.Code, errCheck.Description) } diff --git a/exchanges/itbit/itbit_test.go b/exchanges/itbit/itbit_test.go index 3225ff49..9ba1b823 100644 --- a/exchanges/itbit/itbit_test.go +++ b/exchanges/itbit/itbit_test.go @@ -187,40 +187,35 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := i.GetFee(feeBuilder); resp != float64(0.0035) || err != nil { + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0035), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := i.GetFee(feeBuilder); resp != float64(3500) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3500), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -228,24 +223,21 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -253,8 +245,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := i.GetFee(feeBuilder); resp != float64(40) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(40), resp) + if _, err := i.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index 4c7aa5df..332e5ad2 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -466,25 +466,25 @@ func (i *ItBit) GetOrderInfo(orderID string, pair currency.Pair, assetType asset // NOTE: This has not been implemented due to the fact you need to generate a // a specific wallet ID and they restrict the amount of deposit address you can // request limiting them to 2. -func (i *ItBit) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { +func (i *ItBit) GetDepositAddress(_ currency.Code, _ string) (string, error) { return "", common.ErrNotYetImplemented } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (i *ItBit) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (i *ItBit) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (i *ItBit) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (i *ItBit) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (i *ItBit) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (i *ItBit) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/kraken/README.md b/exchanges/kraken/README.md index d247e18a..e266b0e0 100644 --- a/exchanges/kraken/README.md +++ b/exchanges/kraken/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Kraken - + [![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) diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 7396c26b..f98678b6 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -971,8 +971,7 @@ func (k *Kraken) SendHTTPRequest(ep exchange.URL, path string, result interface{ // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (k *Kraken) SendAuthenticatedHTTPRequest(ep exchange.URL, method string, params url.Values, result interface{}) error { if !k.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - k.Name) + return fmt.Errorf("%s %w", k.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := k.API.Endpoints.GetURL(ep) if err != nil { @@ -1060,7 +1059,7 @@ func (k *Kraken) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { } } } - case exchange.CyptocurrencyDepositFee: + case exchange.CryptocurrencyDepositFee: fee = getCryptocurrencyDepositFee(feeBuilder.Pair.Base) case exchange.InternationalBankWithdrawalFee: diff --git a/exchanges/kraken/kraken_futures.go b/exchanges/kraken/kraken_futures.go index 960e42ec..30619d68 100644 --- a/exchanges/kraken/kraken_futures.go +++ b/exchanges/kraken/kraken_futures.go @@ -259,8 +259,7 @@ func (k *Kraken) signFuturesRequest(endpoint, nonce, data string) string { // SendFuturesAuthRequest will send an auth req func (k *Kraken) SendFuturesAuthRequest(method, path string, postData url.Values, data map[string]interface{}, result interface{}) error { if !k.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - k.Name) + return fmt.Errorf("%s %w", k.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } if postData == nil { postData = url.Values{} diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index bbe47e73..b4cd09f5 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -698,59 +698,52 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := k.GetFee(feeBuilder); resp != float64(0.0026) || err != nil { + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0026), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := k.GetFee(feeBuilder); resp != float64(2600) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2600), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := k.GetFee(feeBuilder); resp != float64(0.0016) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0016), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := k.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := k.GetFee(feeBuilder); resp != float64(5) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(5), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee feeBuilder.Pair.Base = currency.XXBT - if resp, err := k.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(5), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := k.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -758,8 +751,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := k.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -767,8 +759,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := k.GetFee(feeBuilder); resp != float64(5) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(5), resp) + if _, err := k.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/lakebtc/README.md b/exchanges/lakebtc/README.md index a5ad2281..6b762b94 100644 --- a/exchanges/lakebtc/README.md +++ b/exchanges/lakebtc/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Lakebtc - + [![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) diff --git a/exchanges/lakebtc/lakebtc.go b/exchanges/lakebtc/lakebtc.go index 7a481ffd..d091c97f 100644 --- a/exchanges/lakebtc/lakebtc.go +++ b/exchanges/lakebtc/lakebtc.go @@ -289,7 +289,7 @@ func (l *LakeBTC) SendHTTPRequest(endpoint exchange.URL, path string, result int // SendAuthenticatedHTTPRequest sends an autheticated HTTP request to a LakeBTC func (l *LakeBTC) SendAuthenticatedHTTPRequest(ep exchange.URL, method, params string, result interface{}) (err error) { if !l.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) + return fmt.Errorf("%s %w", l.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := l.API.Endpoints.GetURL(ep) if err != nil { @@ -341,7 +341,7 @@ func (l *LakeBTC) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount, feeBuilder.IsMaker) - case exchange.CyptocurrencyDepositFee: + case exchange.CryptocurrencyDepositFee: fee = getCryptocurrencyWithdrawalFee(feeBuilder.Pair.Base) case exchange.InternationalBankWithdrawalFee: // fees for withdrawals are dynamic. They cannot be calculated in diff --git a/exchanges/lakebtc/lakebtc_test.go b/exchanges/lakebtc/lakebtc_test.go index a9e5ff97..d5009b99 100644 --- a/exchanges/lakebtc/lakebtc_test.go +++ b/exchanges/lakebtc/lakebtc_test.go @@ -181,40 +181,35 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := l.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := l.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := l.GetFee(feeBuilder); resp != float64(0.0015) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -222,33 +217,27 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index 09c48329..93b69159 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -508,7 +508,6 @@ func (l *LakeBTC) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - if withdrawRequest.Currency != currency.BTC { return nil, errors.New("only BTC supported for withdrawals") } @@ -525,13 +524,13 @@ func (l *LakeBTC) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (l *LakeBTC) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (l *LakeBTC) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (l *LakeBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (l *LakeBTC) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/lbank/README.md b/exchanges/lbank/README.md index c5be0402..1ea23d3f 100644 --- a/exchanges/lbank/README.md +++ b/exchanges/lbank/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Lbank - + [![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) diff --git a/exchanges/lbank/lbank.go b/exchanges/lbank/lbank.go index 1cd12b0b..282c8b15 100644 --- a/exchanges/lbank/lbank.go +++ b/exchanges/lbank/lbank.go @@ -556,7 +556,7 @@ func (l *Lbank) sign(data string) (string, error) { // SendAuthHTTPRequest sends an authenticated request func (l *Lbank) SendAuthHTTPRequest(method, endpoint string, vals url.Values, result interface{}) error { if !l.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) + return fmt.Errorf("%s %w", l.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } if vals == nil { diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index 0a1f366f..044e07bd 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -609,7 +609,6 @@ func (l *Lbank) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) ( if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := l.Withdraw(withdrawRequest.Crypto.Address, withdrawRequest.Currency.String(), strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64), "", withdrawRequest.Description, "") @@ -623,13 +622,13 @@ func (l *Lbank) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) ( // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (l *Lbank) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (l *Lbank) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (l *Lbank) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (l *Lbank) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/localbitcoins/README.md b/exchanges/localbitcoins/README.md index abafbbd7..91500ccb 100644 --- a/exchanges/localbitcoins/README.md +++ b/exchanges/localbitcoins/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Localbitcoins - + [![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) diff --git a/exchanges/localbitcoins/localbitcoins.go b/exchanges/localbitcoins/localbitcoins.go index 6a509b84..b5ffaa30 100644 --- a/exchanges/localbitcoins/localbitcoins.go +++ b/exchanges/localbitcoins/localbitcoins.go @@ -745,7 +745,7 @@ func (l *LocalBitcoins) SendHTTPRequest(endpoint exchange.URL, path string, resu // localbitcoins func (l *LocalBitcoins) SendAuthenticatedHTTPRequest(ep exchange.URL, method, path string, params url.Values, result interface{}) (err error) { if !l.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) + return fmt.Errorf("%s %w", l.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := l.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/localbitcoins/localbitcoins_test.go b/exchanges/localbitcoins/localbitcoins_test.go index 8acdc733..46a7daf4 100644 --- a/exchanges/localbitcoins/localbitcoins_test.go +++ b/exchanges/localbitcoins/localbitcoins_test.go @@ -137,40 +137,35 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -178,33 +173,26 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := l.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index ed162069..dc431da7 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -474,7 +474,6 @@ func (l *LocalBitcoins) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Re if err := withdrawRequest.Validate(); err != nil { return nil, err } - err := l.WalletSend(withdrawRequest.Crypto.Address, withdrawRequest.Amount, withdrawRequest.PIN) @@ -486,13 +485,13 @@ func (l *LocalBitcoins) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Re // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (l *LocalBitcoins) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (l *LocalBitcoins) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (l *LocalBitcoins) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (l *LocalBitcoins) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/mock/README.md b/exchanges/mock/README.md index f0d35f46..5b51a626 100644 --- a/exchanges/mock/README.md +++ b/exchanges/mock/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Mock - + [![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) diff --git a/exchanges/nonce/README.md b/exchanges/nonce/README.md index d1cc01e1..692d383b 100644 --- a/exchanges/nonce/README.md +++ b/exchanges/nonce/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Nonce - + [![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) diff --git a/exchanges/okcoin/README.md b/exchanges/okcoin/README.md index 8033f0ff..bd920703 100644 --- a/exchanges/okcoin/README.md +++ b/exchanges/okcoin/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Okcoin - + [![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) diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index 1c819c5a..41169115 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -896,52 +896,45 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := o.GetFee(feeBuilder); resp != float64(0.0015) || err != nil { + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := o.GetFee(feeBuilder); resp != float64(1500) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1500), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := o.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/okex/README.md b/exchanges/okex/README.md index ca7ac7bb..a0a1fbcd 100644 --- a/exchanges/okex/README.md +++ b/exchanges/okex/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Okex - + [![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) diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index 1234f549..501faf78 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -1709,58 +1709,47 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := o.GetFee(feeBuilder); resp != float64(0.0015) || err != nil { + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := o.GetFee(feeBuilder); resp != float64(1500) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1500), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := o.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := o.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/okgroup/okgroup.go b/exchanges/okgroup/okgroup.go index 966496e1..aa7758c3 100644 --- a/exchanges/okgroup/okgroup.go +++ b/exchanges/okgroup/okgroup.go @@ -570,8 +570,7 @@ func (o *OKGroup) GetErrorCode(code interface{}) error { // URL arguments must be in the request path and not as url.URL values func (o *OKGroup) SendHTTPRequest(ep exchange.URL, httpMethod, requestType, requestPath string, data, result interface{}, authenticated bool) (err error) { if authenticated && !o.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - o.Name) + return fmt.Errorf("%s %w", o.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := o.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 4e3f234d..0969d7a0 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -423,7 +423,7 @@ func (o *OKGroup) GetOrderInfo(orderID string, pair currency.Pair, assetType ass } // GetDepositAddress returns a deposit address for a specified currency -func (o *OKGroup) GetDepositAddress(p currency.Code, accountID string) (string, error) { +func (o *OKGroup) GetDepositAddress(p currency.Code, _ string) (string, error) { wallet, err := o.GetAccountDepositAddressForCurrency(p.Lower().String()) if err != nil || len(wallet) == 0 { return "", err @@ -437,7 +437,6 @@ func (o *OKGroup) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) if err := withdrawRequest.Validate(); err != nil { return nil, err } - withdrawal, err := o.AccountWithdraw(AccountWithdrawRequest{ Amount: withdrawRequest.Amount, Currency: withdrawRequest.Currency.Lower().String(), @@ -463,13 +462,13 @@ func (o *OKGroup) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (o *OKGroup) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (o *OKGroup) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (o *OKGroup) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (o *OKGroup) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index e1847fc1..7a1d1b4b 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -807,7 +807,7 @@ func TestUpdateOrderFromDetail(t *testing.T) { ExecutedAmount: 0, RemainingAmount: 0, Fee: 0, - Exchange: "", + Exchange: "test", ID: "1", AccountID: "", ClientID: "", @@ -904,7 +904,7 @@ func TestUpdateOrderFromDetail(t *testing.T) { if od.Fee != 1 { t.Error("Failed to update") } - if od.Exchange != "" { + if od.Exchange != "test" { t.Error("Should not be able to update exchange via modify") } if od.ID != "1" { @@ -1000,18 +1000,26 @@ func TestValidationOnOrderTypes(t *testing.T) { } cancelMe = new(Cancel) - if cancelMe.Validate() != ErrPairIsEmpty { - t.Fatal("unexpected error") + err := cancelMe.Validate() + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v'", err, nil) + } + + err = cancelMe.Validate(cancelMe.PairAssetRequired()) + if err == nil || err.Error() != ErrPairIsEmpty.Error() { + t.Errorf("received '%v' expected '%v'", err, ErrPairIsEmpty) } cancelMe.Pair = currency.NewPair(currency.BTC, currency.USDT) - if cancelMe.Validate() != ErrAssetNotSet { - t.Fatal("unexpected error") + err = cancelMe.Validate(cancelMe.PairAssetRequired()) + if err == nil || err.Error() != ErrAssetNotSet.Error() { + t.Errorf("received '%v' expected '%v'", err, ErrAssetNotSet) } cancelMe.AssetType = asset.Spot - if cancelMe.Validate() != nil { - t.Fatal("should not error") + err = cancelMe.Validate(cancelMe.PairAssetRequired()) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v'", err, nil) } if cancelMe.Validate(cancelMe.StandardCancel()) == nil { diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index dc6676ed..09b36ceb 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -202,6 +202,12 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) { d.LastUpdated = m.LastUpdated } } + if d.Exchange == "" { + d.Exchange = m.Exchange + } + if d.ID == "" { + d.ID = m.ID + } } // UpdateOrderFromModify Will update an order detail (used in order management) @@ -710,20 +716,27 @@ func (c *Cancel) StandardCancel() validate.Checker { }) } +// PairAssetRequired is a validation check for when a cancel request +// requires an asset type and currency pair to be present +func (c *Cancel) PairAssetRequired() validate.Checker { + return validate.Check(func() error { + if c.Pair.IsEmpty() { + return ErrPairIsEmpty + } + + if c.AssetType == "" { + return ErrAssetNotSet + } + return nil + }) +} + // Validate checks internal struct requirements func (c *Cancel) Validate(opt ...validate.Checker) error { if c == nil { return ErrCancelOrderIsNil } - if c.Pair.IsEmpty() { - return ErrPairIsEmpty - } - - if c.AssetType == "" { - return ErrAssetNotSet - } - var errs common.Errors for _, o := range opt { err := o.Check() diff --git a/exchanges/orderbook/README.md b/exchanges/orderbook/README.md index 75929294..fcc007fa 100644 --- a/exchanges/orderbook/README.md +++ b/exchanges/orderbook/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Orderbook - + [![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) diff --git a/exchanges/poloniex/README.md b/exchanges/poloniex/README.md index e40201e2..ea8fdde6 100644 --- a/exchanges/poloniex/README.md +++ b/exchanges/poloniex/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Poloniex - + [![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) diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index 076a4304..5a565eda 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -847,8 +847,7 @@ func (p *Poloniex) SendHTTPRequest(ep exchange.URL, path string, result interfac // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (p *Poloniex) SendAuthenticatedHTTPRequest(ep exchange.URL, method, endpoint string, values url.Values, result interface{}) error { if !p.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - p.Name) + return fmt.Errorf("%s %w", p.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } ePoint, err := p.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index 6ea4e946..d1cddff2 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -130,37 +130,29 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() || mockTests { // CryptocurrencyTradeFee Basic - if resp, err := p.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0.0025), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := p.GetFee(feeBuilder); resp != float64(2500) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(2500), resp) + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), resp) + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := p.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0.001), resp) + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -168,27 +160,21 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), resp) + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), resp) + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } @@ -196,9 +182,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", - float64(0), resp) + if _, err := p.GetFee(feeBuilder); err != nil { t.Error(err) } } @@ -470,7 +454,7 @@ func TestWithdraw(t *testing.T) { Address: core.BitcoinDonationAddress, FeeAmount: 1, }, - Amount: 0, + Amount: 0.00001337, Currency: currency.LTC, Description: "WITHDRAW IT ALL", TradePassword: "Password", @@ -485,8 +469,8 @@ func TestWithdraw(t *testing.T) { t.Errorf("Withdraw failed to be placed: %v", err) case !areTestAPIKeysSet() && !mockTests && err == nil: t.Error("Expecting an error when no keys are set") - case mockTests && err == nil: - t.Error("Mock Withdraw() err cannot be nil") + case mockTests && err != nil: + t.Error(err) } } diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index c6974774..0b4a58ee 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -689,7 +689,6 @@ func (p *Poloniex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request if err := withdrawRequest.Validate(); err != nil { return nil, err } - v, err := p.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Amount) if err != nil { return nil, err @@ -701,13 +700,13 @@ func (p *Poloniex) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (p *Poloniex) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (p *Poloniex) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (p *Poloniex) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (p *Poloniex) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/request/README.md b/exchanges/request/README.md index 07fcdc0a..13f3c4a0 100644 --- a/exchanges/request/README.md +++ b/exchanges/request/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Request - + [![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) diff --git a/exchanges/request/request.go b/exchanges/request/request.go index 295b6ff2..5d707f66 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -156,15 +156,15 @@ func (r *Requester) doRequest(req *http.Request, p *Item) error { // Can't currently regenerate nonce and signatures with fresh values for retries, so for now, we must not retry if p.NonceEnabled { if timeoutErr, ok := err.(net.Error); !ok || !timeoutErr.Timeout() { - return fmt.Errorf("request.go error - unable to retry request using nonce, err: %v", err) + return fmt.Errorf("unable to retry request using nonce, err: %v", err) } } if attempt > r.maxRetries { if err != nil { - return fmt.Errorf("request.go error - failed to retry request, err: %v", err) + return fmt.Errorf("failed to retry request, err: %v", err) } - return fmt.Errorf("request.go error - failed to retry request, status: %s", resp.Status) + return fmt.Errorf("failed to retry request, status: %s", resp.Status) } after := RetryAfter(resp, time.Now()) @@ -176,9 +176,9 @@ func (r *Requester) doRequest(req *http.Request, p *Item) error { if d, ok := req.Context().Deadline(); ok && d.After(time.Now()) && time.Now().Add(delay).After(d) { if err != nil { - return fmt.Errorf("request.go error - deadline would be exceeded by retry, err: %v", err) + return fmt.Errorf("deadline would be exceeded by retry, err: %v", err) } - return fmt.Errorf("request.go error - deadline would be exceeded by retry, status: %s", resp.Status) + return fmt.Errorf("deadline would be exceeded by retry, status: %s", resp.Status) } if p.Verbose { diff --git a/exchanges/stats/README.md b/exchanges/stats/README.md index 2e7b74e7..3ce9a3cf 100644 --- a/exchanges/stats/README.md +++ b/exchanges/stats/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Stats - + [![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) diff --git a/exchanges/stream/websocket_types.go b/exchanges/stream/websocket_types.go index 06f08518..45578406 100644 --- a/exchanges/stream/websocket_types.go +++ b/exchanges/stream/websocket_types.go @@ -22,7 +22,7 @@ const ( ) // Websocket defines a return type for websocket connections via the interface -// wrapper for routine processing in routines.go +// wrapper for routine processing type Websocket struct { canUseAuthenticatedEndpoints bool enabled bool diff --git a/exchanges/ticker/README.md b/exchanges/ticker/README.md index fdf8cfab..1eda916c 100644 --- a/exchanges/ticker/README.md +++ b/exchanges/ticker/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Ticker - + [![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) diff --git a/exchanges/ticker/ticker.go b/exchanges/ticker/ticker.go index 7f7947cc..be37acd5 100644 --- a/exchanges/ticker/ticker.go +++ b/exchanges/ticker/ticker.go @@ -81,7 +81,7 @@ func GetTicker(exchange string, p currency.Pair, tickerType asset.Item) (*Price, // list func ProcessTicker(tickerNew *Price) error { if tickerNew.ExchangeName == "" { - return fmt.Errorf(errExchangeNameUnset) + return fmt.Errorf(ErrExchangeNameUnset) } if tickerNew.Pair.IsEmpty() { diff --git a/exchanges/ticker/ticker_types.go b/exchanges/ticker/ticker_types.go index a645cd56..ce30358e 100644 --- a/exchanges/ticker/ticker_types.go +++ b/exchanges/ticker/ticker_types.go @@ -12,7 +12,7 @@ import ( // const values for the ticker package const ( - errExchangeNameUnset = "ticker exchange name not set" + ErrExchangeNameUnset = "ticker exchange name not set" errPairNotSet = "ticker currency pair not set" errAssetTypeNotSet = "ticker asset type not set" errTickerPriceIsNil = "ticker price is nil" diff --git a/exchanges/trade/README.md b/exchanges/trade/README.md index f410ef6a..41616058 100644 --- a/exchanges/trade/README.md +++ b/exchanges/trade/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Trade - + [![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) diff --git a/exchanges/trade/trade.go b/exchanges/trade/trade.go index 656b3eaa..d49b1fa2 100644 --- a/exchanges/trade/trade.go +++ b/exchanges/trade/trade.go @@ -29,7 +29,8 @@ func (p *Processor) setup(wg *sync.WaitGroup) { // AddTradesToBuffer will push trade data onto the buffer func AddTradesToBuffer(exchangeName string, data ...Data) error { - if database.DB == nil || database.DB.Config == nil || !database.DB.Config.Enabled { + cfg := database.DB.GetConfig() + if database.DB == nil || cfg == nil || !cfg.Enabled { return nil } if len(data) == 0 { diff --git a/exchanges/trade/trade_test.go b/exchanges/trade/trade_test.go index 2db63a09..3ea32e80 100644 --- a/exchanges/trade/trade_test.go +++ b/exchanges/trade/trade_test.go @@ -33,9 +33,12 @@ func TestAddTradesToBuffer(t *testing.T) { wg.Add(1) processor.setup(&wg) wg.Wait() - database.DB.Config = &dbConf + err := database.DB.SetConfig(&dbConf) + if err != nil { + t.Error(err) + } cp, _ := currency.NewPairFromString("BTC-USD") - err := AddTradesToBuffer("test!", []Data{ + err = AddTradesToBuffer("test!", []Data{ { Timestamp: time.Now(), Exchange: "test!", diff --git a/exchanges/validate/README.md b/exchanges/validate/README.md index 405994a6..a8e7d657 100644 --- a/exchanges/validate/README.md +++ b/exchanges/validate/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Validate - + [![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) diff --git a/exchanges/yobit/README.md b/exchanges/yobit/README.md index 148eab12..7f870fb6 100644 --- a/exchanges/yobit/README.md +++ b/exchanges/yobit/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Yobit - + [![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) diff --git a/exchanges/yobit/yobit.go b/exchanges/yobit/yobit.go index 77ffd405..48bd1b9a 100644 --- a/exchanges/yobit/yobit.go +++ b/exchanges/yobit/yobit.go @@ -282,7 +282,7 @@ func (y *Yobit) SendHTTPRequest(ep exchange.URL, path string, result interface{} // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to Yobit func (y *Yobit) SendAuthenticatedHTTPRequest(ep exchange.URL, path string, params url.Values, result interface{}) (err error) { if !y.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, y.Name) + return fmt.Errorf("%s %w", y.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := y.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/yobit/yobit_test.go b/exchanges/yobit/yobit_test.go index c0faf3ea..252e1c22 100644 --- a/exchanges/yobit/yobit_test.go +++ b/exchanges/yobit/yobit_test.go @@ -185,134 +185,109 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := y.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := y.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := y.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := y.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyWithdrawalFee Invalid currency feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee QIWI feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.Qiwi - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Wire feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.WireTransfer - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Payeer feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.Payeer - if resp, err := y.GetFee(feeBuilder); resp != float64(0.03) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.03), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Capitalist feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.RUR feeBuilder.BankTransactionType = exchange.Capitalist - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee AdvCash feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.AdvCash - if resp, err := y.GetFee(feeBuilder); resp != float64(0.04) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.04), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee PerfectMoney feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.RUR feeBuilder.BankTransactionType = exchange.PerfectMoney - if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := y.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 24e5c8cb..9e0fdf3a 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -500,7 +500,6 @@ func (y *Yobit) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) ( if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := y.WithdrawCoinsToAddress(withdrawRequest.Currency.String(), withdrawRequest.Amount, withdrawRequest.Crypto.Address) @@ -515,13 +514,13 @@ func (y *Yobit) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) ( // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (y *Yobit) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (y *Yobit) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (y *Yobit) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (y *Yobit) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/zb/README.md b/exchanges/zb/README.md index c15ff351..ebc58847 100644 --- a/exchanges/zb/README.md +++ b/exchanges/zb/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Zb - + [![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) diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index be3dc081..d2c3017f 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -300,7 +300,7 @@ func (z *ZB) SendHTTPRequest(ep exchange.URL, path string, result interface{}, f // SendAuthenticatedHTTPRequest sends authenticated requests to the zb API func (z *ZB) SendAuthenticatedHTTPRequest(ep exchange.URL, httpMethod string, params url.Values, result interface{}, f request.EndpointLimit) error { if !z.AllowAuthenticatedRequest() { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, z.Name) + return fmt.Errorf("%s %w", z.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) } endpoint, err := z.API.Endpoints.GetURL(ep) if err != nil { diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index ef17bf53..b1b47373 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -156,74 +156,58 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if resp, err := z.GetFee(feeBuilder); resp != float64(0.002) || err != nil { + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } - // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := z.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := z.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := z.GetFee(feeBuilder); resp != float64(0.005) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.005), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } - // CryptocurrencyWithdrawalFee Invalid currency feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } - // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + if _, err := z.GetFee(feeBuilder); err != nil { t.Error(err) } } diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 212328e3..69ac853b 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -628,7 +628,6 @@ func (z *ZB) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*wi if err := withdrawRequest.Validate(); err != nil { return nil, err } - v, err := z.Withdraw(withdrawRequest.Currency.Lower().String(), withdrawRequest.Crypto.Address, withdrawRequest.TradePassword, @@ -645,13 +644,13 @@ func (z *ZB) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*wi // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (z *ZB) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (z *ZB) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (z *ZB) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { +func (z *ZB) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } diff --git a/gctrpc/rpc.pb.go b/gctrpc/rpc.pb.go index f9a64a95..ecb7cb45 100644 --- a/gctrpc/rpc.pb.go +++ b/gctrpc/rpc.pb.go @@ -4490,11 +4490,11 @@ type ConditionParams struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Condition string `protobuf:"bytes,1,opt,name=condition,proto3" json:"condition,omitempty"` - Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` - CheckBids bool `protobuf:"varint,3,opt,name=check_bids,json=checkBids,proto3" json:"check_bids,omitempty"` - CheckBidsAndAsks bool `protobuf:"varint,4,opt,name=check_bids_and_asks,json=checkBidsAndAsks,proto3" json:"check_bids_and_asks,omitempty"` - OrderbookAmount float64 `protobuf:"fixed64,5,opt,name=orderbook_amount,json=orderbookAmount,proto3" json:"orderbook_amount,omitempty"` + Condition string `protobuf:"bytes,1,opt,name=condition,proto3" json:"condition,omitempty"` + Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` + CheckBids bool `protobuf:"varint,3,opt,name=check_bids,json=checkBids,proto3" json:"check_bids,omitempty"` + CheckAsks bool `protobuf:"varint,4,opt,name=check_asks,json=checkAsks,proto3" json:"check_asks,omitempty"` + OrderbookAmount float64 `protobuf:"fixed64,5,opt,name=orderbook_amount,json=orderbookAmount,proto3" json:"orderbook_amount,omitempty"` } func (x *ConditionParams) Reset() { @@ -4550,9 +4550,9 @@ func (x *ConditionParams) GetCheckBids() bool { return false } -func (x *ConditionParams) GetCheckBidsAndAsks() bool { +func (x *ConditionParams) GetCheckAsks() bool { if x != nil { - return x.CheckBidsAndAsks + return x.CheckAsks } return false } @@ -9784,16 +9784,15 @@ var file_rpc_proto_rawDesc = []byte{ 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbe, 0x01, 0x0a, 0x0f, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x62, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x42, 0x69, - 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x62, 0x69, 0x64, 0x73, - 0x5f, 0x61, 0x6e, 0x64, 0x5f, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x10, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x42, 0x69, 0x64, 0x73, 0x41, 0x6e, 0x64, 0x41, 0x73, 0x6b, + 0x64, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x61, 0x73, 0x6b, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x41, 0x73, 0x6b, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf5, 0x01, 0x0a, diff --git a/gctrpc/rpc.proto b/gctrpc/rpc.proto index 63c29c87..51e2de6d 100644 --- a/gctrpc/rpc.proto +++ b/gctrpc/rpc.proto @@ -426,7 +426,7 @@ message ConditionParams { string condition = 1; double price = 2; bool check_bids = 3; - bool check_bids_and_asks = 4; + bool check_asks = 4; double orderbook_amount = 5; } diff --git a/gctrpc/rpc.swagger.json b/gctrpc/rpc.swagger.json index bb2bf032..cdeaf552 100644 --- a/gctrpc/rpc.swagger.json +++ b/gctrpc/rpc.swagger.json @@ -2922,7 +2922,7 @@ "check_bids": { "type": "boolean" }, - "check_bids_and_asks": { + "check_asks": { "type": "boolean" }, "orderbook_amount": { diff --git a/gctscript/vm/manager.go b/gctscript/vm/manager.go index 62e03557..477de66e 100644 --- a/gctscript/vm/manager.go +++ b/gctscript/vm/manager.go @@ -6,11 +6,14 @@ import ( "sync" "sync/atomic" - "github.com/thrasher-corp/gocryptotrader/engine/subsystem" "github.com/thrasher-corp/gocryptotrader/log" ) -const gctscriptManagerName = "GCTScript" +const ( + caseName = "GCTScript" + // Name is an exported subsystem name + Name = "gctscript" +) // GctScriptManager loads and runs GCT Tengo scripts type GctScriptManager struct { @@ -31,22 +34,24 @@ func NewManager(config *Config) (*GctScriptManager, error) { }, nil } -// Started returns if gctscript manager subsystem is started -func (g *GctScriptManager) Started() bool { +// IsRunning returns if gctscript manager subsystem is started +func (g *GctScriptManager) IsRunning() bool { + if g == nil { + return false + } return atomic.LoadInt32(&g.started) == 1 } // Start starts gctscript subsystem and creates shutdown channel func (g *GctScriptManager) Start(wg *sync.WaitGroup) (err error) { if !atomic.CompareAndSwapInt32(&g.started, 0, 1) { - return fmt.Errorf("%s %w", gctscriptManagerName, subsystem.ErrSubSystemAlreadyStarted) + return fmt.Errorf("%s %s", caseName, ErrScriptFailedValidation) } defer func() { if err != nil { atomic.CompareAndSwapInt32(&g.started, 1, 0) } }() - log.Debugln(log.Global, gctscriptManagerName, subsystem.MsgSubSystemStarting) g.shutdown = make(chan struct{}) wg.Add(1) @@ -57,13 +62,12 @@ func (g *GctScriptManager) Start(wg *sync.WaitGroup) (err error) { // Stop stops gctscript subsystem along with all running Virtual Machines func (g *GctScriptManager) Stop() error { if atomic.LoadInt32(&g.started) == 0 { - return fmt.Errorf("%s %w", gctscriptManagerName, subsystem.ErrSubSystemNotStarted) + return fmt.Errorf("%s not running", caseName) } defer func() { atomic.CompareAndSwapInt32(&g.started, 1, 0) }() - log.Debugln(log.GCTScriptMgr, gctscriptManagerName, subsystem.MsgSubSystemShuttingDown) err := g.ShutdownAll() if err != nil { return err @@ -73,13 +77,12 @@ func (g *GctScriptManager) Stop() error { } func (g *GctScriptManager) run(wg *sync.WaitGroup) { - log.Debugln(log.Global, gctscriptManagerName, subsystem.MsgSubSystemStarted) + log.Debugf(log.Global, "%s starting", caseName) SetDefaultScriptOutput() g.autoLoad() defer func() { wg.Done() - log.Debugln(log.GCTScriptMgr, gctscriptManagerName, subsystem.MsgSubSystemShutdown) }() <-g.shutdown diff --git a/gctscript/vm/vm.go b/gctscript/vm/vm.go index 45f1c407..6b345c53 100644 --- a/gctscript/vm/vm.go +++ b/gctscript/vm/vm.go @@ -23,7 +23,7 @@ import ( // NewVM attempts to create a new Virtual Machine firstly from pool func (g *GctScriptManager) NewVM() (vm *VM) { - if !g.Started() { + if !g.IsRunning() { log.Error(log.GCTScriptMgr, Error{ Action: "NewVM", Cause: ErrScriptingDisabled, diff --git a/gctscript/wrappers/gct/exchange/exchange.go b/gctscript/wrappers/gct/exchange/exchange.go index a7e00acf..4a555de3 100644 --- a/gctscript/wrappers/gct/exchange/exchange.go +++ b/gctscript/wrappers/gct/exchange/exchange.go @@ -145,7 +145,7 @@ func (e Exchange) DepositAddress(exch string, currencyCode currency.Code) (out s err = errors.New("currency code is empty") return } - return engine.Bot.DepositAddressManager.GetDepositAddressByExchange(exch, currencyCode) + return engine.Bot.DepositAddressManager.GetDepositAddressByExchangeAndCurrency(exch, currencyCode) } // WithdrawalFiatFunds withdraw funds from exchange to requested fiat source @@ -163,7 +163,7 @@ func (e Exchange) WithdrawalFiatFunds(bankAccountID string, request *withdraw.Re } } - otp, err := engine.Bot.GetExchangeoOTPByName(request.Exchange) + otp, err := engine.Bot.GetExchangeOTPByName(request.Exchange) if err == nil { otpValue, errParse := strconv.ParseInt(otp, 10, 64) if errParse != nil { @@ -182,7 +182,7 @@ func (e Exchange) WithdrawalFiatFunds(bankAccountID string, request *withdraw.Re request.Fiat.Bank.SWIFTCode = v.SWIFTCode request.Fiat.Bank.IBAN = v.IBAN - resp, err := engine.Bot.SubmitWithdrawal(request) + resp, err := engine.Bot.WithdrawManager.SubmitWithdrawal(request) if err != nil { return "", err } @@ -196,7 +196,7 @@ func (e Exchange) WithdrawalCryptoFunds(request *withdraw.Request) (string, erro if err != nil { return "", err } - otp, err := engine.Bot.GetExchangeoOTPByName(request.Exchange) + otp, err := engine.Bot.GetExchangeOTPByName(request.Exchange) if err == nil { v, errParse := strconv.ParseInt(otp, 10, 64) if errParse != nil { @@ -205,7 +205,7 @@ func (e Exchange) WithdrawalCryptoFunds(request *withdraw.Request) (string, erro request.OneTimePassword = v } - resp, err := engine.Bot.SubmitWithdrawal(request) + resp, err := engine.Bot.WithdrawManager.SubmitWithdrawal(request) if err != nil { return "", err } diff --git a/gctscript/wrappers/gct/exchange/exchange_test.go b/gctscript/wrappers/gct/exchange/exchange_test.go index b52f8d7f..4060d377 100644 --- a/gctscript/wrappers/gct/exchange/exchange_test.go +++ b/gctscript/wrappers/gct/exchange/exchange_test.go @@ -60,8 +60,8 @@ func TestExchange_Exchanges(t *testing.T) { t.Parallel() x := exchangeTest.Exchanges(false) y := len(x) - if y != 28 { - t.Fatalf("expected 28 received %v", y) + if y != 1 { + t.Fatalf("expected 1 received %v", y) } } @@ -206,6 +206,9 @@ func setupEngine() (err error) { return err } + em := engine.SetupExchangeManager() + engine.Bot.ExchangeManager = em + return engine.Bot.LoadExchange(exchName, false, nil) } diff --git a/gctscript/wrappers/gct/gctwrapper_test.go b/gctscript/wrappers/gct/gctwrapper_test.go index 966b8c6c..1562ecf4 100644 --- a/gctscript/wrappers/gct/gctwrapper_test.go +++ b/gctscript/wrappers/gct/gctwrapper_test.go @@ -30,10 +30,34 @@ func TestMain(m *testing.M) { log.Print(err) os.Exit(1) } - engine.Bot.LoadExchange(exch.Value, false, nil) - engine.Bot.DepositAddressManager = new(engine.DepositAddressManager) - go engine.Bot.DepositAddressManager.Sync() - err = engine.Bot.OrderManager.Start(engine.Bot) + em := engine.SetupExchangeManager() + exch, err := em.NewExchangeByName(exch.Value) + if err != nil { + log.Print(err) + os.Exit(1) + } + exch.SetDefaults() + em.Add(exch) + engine.Bot.ExchangeManager = em + engine.Bot.WithdrawManager, err = engine.SetupWithdrawManager(em, nil, true) + if err != nil { + log.Print(err) + os.Exit(1) + } + + engine.Bot.DepositAddressManager = engine.SetupDepositAddressManager() + err = engine.Bot.DepositAddressManager.Sync(engine.Bot.GetExchangeCryptocurrencyDepositAddresses()) + if err != nil { + log.Print(err) + os.Exit(1) + } + + engine.Bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &engine.Bot.ServicesWG, false) + if err != nil { + log.Print(err) + os.Exit(1) + } + err = engine.Bot.OrderManager.Start() if err != nil { log.Print(err) os.Exit(1) @@ -46,7 +70,7 @@ func TestSetup(t *testing.T) { x := Setup() xType := reflect.TypeOf(x).String() if xType != "*gct.Wrapper" { - t.Fatalf("Setup() should return pointer to Wrapper instead received: %v", x) + t.Fatalf("SetupCommunicationManager() should return pointer to Wrapper instead received: %v", x) } } diff --git a/log/logger_setup.go b/log/logger_setup.go index 96b3dab1..2eb306a5 100644 --- a/log/logger_setup.go +++ b/log/logger_setup.go @@ -141,6 +141,7 @@ func init() { ConnectionMgr = registerNewSubLogger("CONNECTION") BackTester = registerNewSubLogger("BACKTESTER") CommunicationMgr = registerNewSubLogger("COMMS") + APIServerMgr = registerNewSubLogger("API") ConfigMgr = registerNewSubLogger("CONFIG") DatabaseMgr = registerNewSubLogger("DATABASE") OrderMgr = registerNewSubLogger("ORDER") diff --git a/log/sublogger_types.go b/log/sublogger_types.go index 67c9a502..e96ad948 100644 --- a/log/sublogger_types.go +++ b/log/sublogger_types.go @@ -10,6 +10,7 @@ var ( BackTester *subLogger ConnectionMgr *subLogger CommunicationMgr *subLogger + APIServerMgr *subLogger ConfigMgr *subLogger DatabaseMgr *subLogger GCTScriptMgr *subLogger diff --git a/ntpclient/ntpclient.go b/ntpclient/ntpclient.go deleted file mode 100644 index 2ede2745..00000000 --- a/ntpclient/ntpclient.go +++ /dev/null @@ -1,65 +0,0 @@ -package ntpclient - -import ( - "encoding/binary" - "net" - "time" - - "github.com/thrasher-corp/gocryptotrader/log" -) - -type ntppacket struct { - Settings uint8 // leap yr indicator, ver number, and mode - Stratum uint8 // stratum of local clock - Poll int8 // poll exponent - Precision int8 // precision exponent - RootDelay uint32 // root delay - RootDispersion uint32 // root dispersion - ReferenceID uint32 // reference id - RefTimeSec uint32 // reference timestamp sec - RefTimeFrac uint32 // reference timestamp fractional - OrigTimeSec uint32 // origin time secs - OrigTimeFrac uint32 // origin time fractional - RxTimeSec uint32 // receive time secs - RxTimeFrac uint32 // receive time frac - TxTimeSec uint32 // transmit time secs - TxTimeFrac uint32 // transmit time frac -} - -// NTPClient create's a new NTPClient and returns local based on ntp servers provided timestamp -// if no server can be reached will return local time in UTC() -func NTPClient(pool []string) time.Time { - for i := range pool { - con, err := net.DialTimeout("udp", pool[i], 5*time.Second) - if err != nil { - log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", pool[i]) - continue - } - - if err := con.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { - log.Warnf(log.TimeMgr, "Unable to SetDeadline. Error: %s\n", err) - con.Close() - continue - } - - req := &ntppacket{Settings: 0x1B} - if err := binary.Write(con, binary.BigEndian, req); err != nil { - con.Close() - continue - } - - rsp := &ntppacket{} - if err := binary.Read(con, binary.BigEndian, rsp); err != nil { - con.Close() - continue - } - - secs := float64(rsp.TxTimeSec) - 2208988800 - nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32 - - con.Close() - return time.Unix(int64(secs), nanos) - } - log.Warnln(log.TimeMgr, "No valid NTP servers found, using current system time") - return time.Now().UTC() -} diff --git a/ntpclient/ntpclient_test.go b/ntpclient/ntpclient_test.go deleted file mode 100644 index 80b5341d..00000000 --- a/ntpclient/ntpclient_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package ntpclient - -import ( - "reflect" - "testing" - "time" -) - -func TestNTPClient(t *testing.T) { - pool := []string{"pool.ntp.org:123", "0.pool.ntp.org:123"} - v := NTPClient(pool) - - if reflect.TypeOf(v) != reflect.TypeOf(time.Time{}) { - t.Errorf("NTPClient should return time.Time{}") - } - - if v.IsZero() { - t.Error("NTPClient should return valid time received zero value") - } - - const timeFormat = "2006-01-02T15:04" - - if v.UTC().Format(timeFormat) != time.Now().UTC().Format(timeFormat) { - t.Errorf("NTPClient returned incorrect time received: %v", v.UTC().Format(timeFormat)) - } -} diff --git a/portfolio/README.md b/portfolio/README.md index 22ef66ff..ba24c852 100644 --- a/portfolio/README.md +++ b/portfolio/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Portfolio - + [![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) diff --git a/portfolio/portfolio.go b/portfolio/portfolio.go index c9e3e753..c7e23602 100644 --- a/portfolio/portfolio.go +++ b/portfolio/portfolio.go @@ -18,24 +18,20 @@ const ( ethplorerAPIURL = "https://api.ethplorer.io" ethplorerAddressInfo = "getAddressInfo" - // PortfolioAddressExchange is a label for an exchange address - PortfolioAddressExchange = "Exchange" - // PortfolioAddressPersonal is a label for a personal/offline address - PortfolioAddressPersonal = "Personal" + // ExchangeAddress is a label for an exchange address + ExchangeAddress = "Exchange" + // PersonalAddress is a label for a personal/offline address + PersonalAddress = "Personal" ) -// Portfolio is variable store holding an array of portfolioAddress -var Portfolio Base - -// Verbose allows for debug output when sending an http request -var Verbose bool +var errNotEthAddress = errors.New("not an Ethereum address") // GetEthereumBalance single or multiple address information as // EtherchainBalanceResponse -func GetEthereumBalance(address string) (EthplorerResponse, error) { +func (b *Base) GetEthereumBalance(address string) (EthplorerResponse, error) { valid, _ := common.IsValidCryptoAddress(address, "eth") if !valid { - return EthplorerResponse{}, errors.New("not an Ethereum address") + return EthplorerResponse{}, errNotEthAddress } urlPath := fmt.Sprintf( @@ -43,12 +39,12 @@ func GetEthereumBalance(address string) (EthplorerResponse, error) { ) result := EthplorerResponse{} - return result, common.SendHTTPGetRequest(urlPath, true, Verbose, &result) + return result, common.SendHTTPGetRequest(urlPath, true, b.Verbose, &result) } // GetCryptoIDAddress queries CryptoID for an address balance for a // specified cryptocurrency -func GetCryptoIDAddress(address string, coinType currency.Code) (float64, error) { +func (b *Base) GetCryptoIDAddress(address string, coinType currency.Code) (float64, error) { ok, err := common.IsValidCryptoAddress(address, coinType.String()) if !ok || err != nil { return 0, errors.New("invalid address") @@ -60,7 +56,7 @@ func GetCryptoIDAddress(address string, coinType currency.Code) (float64, error) coinType.Lower(), address) - err = common.SendHTTPGetRequest(url, true, Verbose, &result) + err = common.SendHTTPGetRequest(url, true, b.Verbose, &result) if err != nil { return 0, err } @@ -68,9 +64,9 @@ func GetCryptoIDAddress(address string, coinType currency.Code) (float64, error) } // GetRippleBalance returns the value for a ripple address -func GetRippleBalance(address string) (float64, error) { +func (b *Base) GetRippleBalance(address string) (float64, error) { var result XRPScanAccount - err := common.SendHTTPGetRequest(xrpScanAPIURL+address, true, Verbose, &result) + err := common.SendHTTPGetRequest(xrpScanAPIURL+address, true, b.Verbose, &result) if err != nil { return 0, err } @@ -84,21 +80,21 @@ func GetRippleBalance(address string) (float64, error) { // GetAddressBalance acceses the portfolio base and returns the balance by passed // in address, coin type and description -func (p *Base) GetAddressBalance(address, description string, coinType currency.Code) (float64, bool) { - for x := range p.Addresses { - if p.Addresses[x].Address == address && - p.Addresses[x].Description == description && - p.Addresses[x].CoinType == coinType { - return p.Addresses[x].Balance, true +func (b *Base) GetAddressBalance(address, description string, coinType currency.Code) (float64, bool) { + for x := range b.Addresses { + if b.Addresses[x].Address == address && + b.Addresses[x].Description == description && + b.Addresses[x].CoinType == coinType { + return b.Addresses[x].Balance, true } } return 0, false } // ExchangeExists checks to see if an exchange exists in the portfolio base -func (p *Base) ExchangeExists(exchangeName string) bool { - for x := range p.Addresses { - if p.Addresses[x].Address == exchangeName { +func (b *Base) ExchangeExists(exchangeName string) bool { + for x := range b.Addresses { + if b.Addresses[x].Address == exchangeName { return true } } @@ -107,9 +103,9 @@ func (p *Base) ExchangeExists(exchangeName string) bool { // AddressExists checks to see if there is an address associated with the // portfolio base -func (p *Base) AddressExists(address string) bool { - for x := range p.Addresses { - if p.Addresses[x].Address == address { +func (b *Base) AddressExists(address string) bool { + for x := range b.Addresses { + if b.Addresses[x].Address == address { return true } } @@ -118,9 +114,9 @@ func (p *Base) AddressExists(address string) bool { // ExchangeAddressExists checks to see if there is an exchange address // associated with the portfolio base -func (p *Base) ExchangeAddressExists(exchangeName string, coinType currency.Code) bool { - for x := range p.Addresses { - if p.Addresses[x].Address == exchangeName && p.Addresses[x].CoinType == coinType { +func (b *Base) ExchangeAddressExists(exchangeName string, coinType currency.Code) bool { + for x := range b.Addresses { + if b.Addresses[x].Address == exchangeName && b.Addresses[x].CoinType == coinType { return true } } @@ -128,31 +124,31 @@ func (p *Base) ExchangeAddressExists(exchangeName string, coinType currency.Code } // AddExchangeAddress adds an exchange address to the portfolio base -func (p *Base) AddExchangeAddress(exchangeName string, coinType currency.Code, balance float64) { - if p.ExchangeAddressExists(exchangeName, coinType) { - p.UpdateExchangeAddressBalance(exchangeName, coinType, balance) +func (b *Base) AddExchangeAddress(exchangeName string, coinType currency.Code, balance float64) { + if b.ExchangeAddressExists(exchangeName, coinType) { + b.UpdateExchangeAddressBalance(exchangeName, coinType, balance) } else { - p.Addresses = append( - p.Addresses, Address{Address: exchangeName, CoinType: coinType, - Balance: balance, Description: PortfolioAddressExchange}, + b.Addresses = append( + b.Addresses, Address{Address: exchangeName, CoinType: coinType, + Balance: balance, Description: ExchangeAddress}, ) } } // UpdateAddressBalance updates the portfolio base balance -func (p *Base) UpdateAddressBalance(address string, amount float64) { - for x := range p.Addresses { - if p.Addresses[x].Address == address { - p.Addresses[x].Balance = amount +func (b *Base) UpdateAddressBalance(address string, amount float64) { + for x := range b.Addresses { + if b.Addresses[x].Address == address { + b.Addresses[x].Balance = amount } } } // RemoveExchangeAddress removes an exchange address from the portfolio. -func (p *Base) RemoveExchangeAddress(exchangeName string, coinType currency.Code) { - for x := range p.Addresses { - if p.Addresses[x].Address == exchangeName && p.Addresses[x].CoinType == coinType { - p.Addresses = append(p.Addresses[:x], p.Addresses[x+1:]...) +func (b *Base) RemoveExchangeAddress(exchangeName string, coinType currency.Code) { + for x := range b.Addresses { + if b.Addresses[x].Address == exchangeName && b.Addresses[x].CoinType == coinType { + b.Addresses = append(b.Addresses[:x], b.Addresses[x+1:]...) return } } @@ -160,16 +156,16 @@ func (p *Base) RemoveExchangeAddress(exchangeName string, coinType currency.Code // UpdateExchangeAddressBalance updates the portfolio balance when checked // against correct exchangeName and coinType. -func (p *Base) UpdateExchangeAddressBalance(exchangeName string, coinType currency.Code, balance float64) { - for x := range p.Addresses { - if p.Addresses[x].Address == exchangeName && p.Addresses[x].CoinType == coinType { - p.Addresses[x].Balance = balance +func (b *Base) UpdateExchangeAddressBalance(exchangeName string, coinType currency.Code, balance float64) { + for x := range b.Addresses { + if b.Addresses[x].Address == exchangeName && b.Addresses[x].CoinType == coinType { + b.Addresses[x].Balance = balance } } } // AddAddress adds an address to the portfolio base -func (p *Base) AddAddress(address, description string, coinType currency.Code, balance float64) error { +func (b *Base) AddAddress(address, description string, coinType currency.Code, balance float64) error { if address == "" { return errors.New("address is empty") } @@ -178,19 +174,22 @@ func (p *Base) AddAddress(address, description string, coinType currency.Code, b return errors.New("coin type is empty") } - if description == PortfolioAddressExchange { - p.AddExchangeAddress(address, coinType, balance) + if description == ExchangeAddress { + b.AddExchangeAddress(address, coinType, balance) } - if !p.AddressExists(address) { - p.Addresses = append( - p.Addresses, Address{Address: address, CoinType: coinType, + if !b.AddressExists(address) { + b.Addresses = append( + b.Addresses, Address{Address: address, CoinType: coinType, Balance: balance, Description: description}, ) } else { if balance <= 0 { - p.RemoveAddress(address, description, coinType) + err := b.RemoveAddress(address, description, coinType) + if err != nil { + return err + } } else { - p.UpdateAddressBalance(address, balance) + b.UpdateAddressBalance(address, balance) } } return nil @@ -198,7 +197,7 @@ func (p *Base) AddAddress(address, description string, coinType currency.Code, b // RemoveAddress removes an address when checked against the correct address and // coinType -func (p *Base) RemoveAddress(address, description string, coinType currency.Code) error { +func (b *Base) RemoveAddress(address, description string, coinType currency.Code) error { if address == "" { return errors.New("address is empty") } @@ -207,11 +206,11 @@ func (p *Base) RemoveAddress(address, description string, coinType currency.Code return errors.New("coin type is empty") } - for x := range p.Addresses { - if p.Addresses[x].Address == address && - p.Addresses[x].CoinType == coinType && - p.Addresses[x].Description == description { - p.Addresses = append(p.Addresses[:x], p.Addresses[x+1:]...) + for x := range b.Addresses { + if b.Addresses[x].Address == address && + b.Addresses[x].CoinType == coinType && + b.Addresses[x].Description == description { + b.Addresses = append(b.Addresses[:x], b.Addresses[x+1:]...) return nil } } @@ -220,16 +219,16 @@ func (p *Base) RemoveAddress(address, description string, coinType currency.Code } // UpdatePortfolio adds to the portfolio addresses by coin type -func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error { - if strings.Contains(strings.Join(addresses, ","), PortfolioAddressExchange) || - strings.Contains(strings.Join(addresses, ","), PortfolioAddressPersonal) { +func (b *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error { + if strings.Contains(strings.Join(addresses, ","), ExchangeAddress) || + strings.Contains(strings.Join(addresses, ","), PersonalAddress) { return nil } switch coinType { case currency.ETH: for x := range addresses { - result, err := GetEthereumBalance(addresses[x]) + result, err := b.GetEthereumBalance(addresses[x]) if err != nil { return err } @@ -238,8 +237,8 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error return errors.New(result.Error.Message) } - err = p.AddAddress(addresses[x], - PortfolioAddressPersonal, + err = b.AddAddress(addresses[x], + PersonalAddress, coinType, result.ETH.Balance) if err != nil { @@ -248,12 +247,12 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error } case currency.XRP: for x := range addresses { - result, err := GetRippleBalance(addresses[x]) + result, err := b.GetRippleBalance(addresses[x]) if err != nil { return err } - err = p.AddAddress(addresses[x], - PortfolioAddressPersonal, + err = b.AddAddress(addresses[x], + PersonalAddress, coinType, result) if err != nil { @@ -262,12 +261,12 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error } default: for x := range addresses { - result, err := GetCryptoIDAddress(addresses[x], coinType) + result, err := b.GetCryptoIDAddress(addresses[x], coinType) if err != nil { return err } - err = p.AddAddress(addresses[x], - PortfolioAddressPersonal, + err = b.AddAddress(addresses[x], + PersonalAddress, coinType, result) if err != nil { @@ -279,45 +278,45 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error } // GetPortfolioByExchange returns currency portfolio amount by exchange -func (p *Base) GetPortfolioByExchange(exchangeName string) map[currency.Code]float64 { +func (b *Base) GetPortfolioByExchange(exchangeName string) map[currency.Code]float64 { result := make(map[currency.Code]float64) - for x := range p.Addresses { - if strings.Contains(p.Addresses[x].Address, exchangeName) { - result[p.Addresses[x].CoinType] = p.Addresses[x].Balance + for x := range b.Addresses { + if strings.Contains(b.Addresses[x].Address, exchangeName) { + result[b.Addresses[x].CoinType] = b.Addresses[x].Balance } } return result } // GetExchangePortfolio returns current portfolio base information -func (p *Base) GetExchangePortfolio() map[currency.Code]float64 { +func (b *Base) GetExchangePortfolio() map[currency.Code]float64 { result := make(map[currency.Code]float64) - for _, x := range p.Addresses { - if x.Description != PortfolioAddressExchange { + for i := range b.Addresses { + if b.Addresses[i].Description != ExchangeAddress { continue } - balance, ok := result[x.CoinType] + balance, ok := result[b.Addresses[i].CoinType] if !ok { - result[x.CoinType] = x.Balance + result[b.Addresses[i].CoinType] = b.Addresses[i].Balance } else { - result[x.CoinType] = x.Balance + balance + result[b.Addresses[i].CoinType] = b.Addresses[i].Balance + balance } } return result } // GetPersonalPortfolio returns current portfolio base information -func (p *Base) GetPersonalPortfolio() map[currency.Code]float64 { +func (b *Base) GetPersonalPortfolio() map[currency.Code]float64 { result := make(map[currency.Code]float64) - for _, x := range p.Addresses { - if x.Description == PortfolioAddressExchange { + for i := range b.Addresses { + if strings.EqualFold(b.Addresses[i].Description, ExchangeAddress) { continue } - balance, ok := result[x.CoinType] + balance, ok := result[b.Addresses[i].CoinType] if !ok { - result[x.CoinType] = x.Balance + result[b.Addresses[i].CoinType] = b.Addresses[i].Balance } else { - result[x.CoinType] = x.Balance + balance + result[b.Addresses[i].CoinType] = b.Addresses[i].Balance + balance } } return result @@ -342,9 +341,9 @@ func getPercentageSpecific(input float64, target currency.Code, totals map[curre // GetPortfolioSummary returns the complete portfolio summary, showing // coin totals, offline and online summaries with their relative percentages. -func (p *Base) GetPortfolioSummary() Summary { - personalHoldings := p.GetPersonalPortfolio() - exchangeHoldings := p.GetExchangePortfolio() +func (b *Base) GetPortfolioSummary() Summary { + personalHoldings := b.GetPersonalPortfolio() + exchangeHoldings := b.GetExchangePortfolio() totalCoins := make(map[currency.Code]float64) for x, y := range personalHoldings { @@ -385,10 +384,10 @@ func (p *Base) GetPortfolioSummary() Summary { } var portfolioExchanges []string - for _, x := range p.Addresses { - if x.Description == PortfolioAddressExchange { - if !common.StringDataCompare(portfolioExchanges, x.Address) { - portfolioExchanges = append(portfolioExchanges, x.Address) + for i := range b.Addresses { + if strings.EqualFold(b.Addresses[i].Description, ExchangeAddress) { + if !common.StringDataCompare(portfolioExchanges, b.Addresses[i].Address) { + portfolioExchanges = append(portfolioExchanges, b.Addresses[i].Address) } } } @@ -396,7 +395,7 @@ func (p *Base) GetPortfolioSummary() Summary { exchangeSummary := make(map[string]map[currency.Code]OnlineCoinSummary) for x := range portfolioExchanges { exchgName := portfolioExchanges[x] - result := p.GetPortfolioByExchange(exchgName) + result := b.GetPortfolioByExchange(exchgName) coinSummary := make(map[currency.Code]OnlineCoinSummary) for y, z := range result { @@ -411,21 +410,21 @@ func (p *Base) GetPortfolioSummary() Summary { portfolioOutput.OnlineSummary = exchangeSummary offlineSummary := make(map[currency.Code][]OfflineCoinSummary) - for _, x := range p.Addresses { - if x.Description != PortfolioAddressExchange { + for i := range b.Addresses { + if !strings.EqualFold(b.Addresses[i].Description, ExchangeAddress) { coinSummary := OfflineCoinSummary{ - Address: x.Address, - Balance: x.Balance, - Percentage: getPercentageSpecific(x.Balance, x.CoinType, + Address: b.Addresses[i].Address, + Balance: b.Addresses[i].Balance, + Percentage: getPercentageSpecific(b.Addresses[i].Balance, b.Addresses[i].CoinType, totalCoins), } - result, ok := offlineSummary[x.CoinType] + result, ok := offlineSummary[b.Addresses[i].CoinType] if !ok { - offlineSummary[x.CoinType] = append(offlineSummary[x.CoinType], + offlineSummary[b.Addresses[i].CoinType] = append(offlineSummary[b.Addresses[i].CoinType], coinSummary) } else { result = append(result, coinSummary) - offlineSummary[x.CoinType] = result + offlineSummary[b.Addresses[i].CoinType] = result } } } @@ -434,33 +433,33 @@ func (p *Base) GetPortfolioSummary() Summary { } // GetPortfolioGroupedCoin returns portfolio base information grouped by coin -func (p *Base) GetPortfolioGroupedCoin() map[currency.Code][]string { +func (b *Base) GetPortfolioGroupedCoin() map[currency.Code][]string { result := make(map[currency.Code][]string) - for _, x := range p.Addresses { - if strings.Contains(x.Description, PortfolioAddressExchange) { + for i := range b.Addresses { + if strings.EqualFold(b.Addresses[i].Description, ExchangeAddress) { continue } - result[x.CoinType] = append(result[x.CoinType], x.Address) + result[b.Addresses[i].CoinType] = append(result[b.Addresses[i].CoinType], b.Addresses[i].Address) } return result } // Seed appends a portfolio base object with another base portfolio // addresses -func (p *Base) Seed(port Base) { - p.Addresses = port.Addresses +func (b *Base) Seed(port Base) { + b.Addresses = port.Addresses } // StartPortfolioWatcher observes the portfolio object -func StartPortfolioWatcher() { - addrCount := len(Portfolio.Addresses) +func (b *Base) StartPortfolioWatcher() { + addrCount := len(b.Addresses) log.Debugf(log.PortfolioMgr, "PortfolioWatcher started: Have %d entries in portfolio.\n", addrCount, ) for { - data := Portfolio.GetPortfolioGroupedCoin() + data := b.GetPortfolioGroupedCoin() for key, value := range data { - err := Portfolio.UpdatePortfolio(value, key) + err := b.UpdatePortfolio(value, key) if err != nil { log.Errorf(log.PortfolioMgr, "PortfolioWatcher error %s for currency %s, val %v\n", @@ -479,41 +478,36 @@ func StartPortfolioWatcher() { } } -// GetPortfolio returns a pointer to the portfolio base -func GetPortfolio() *Base { - return &Portfolio -} - // IsExchangeSupported checks if exchange is supported by portfolio address -func IsExchangeSupported(exchange, address string) (ret bool) { - for x := range Portfolio.Addresses { - if Portfolio.Addresses[x].Address != address { +func (b *Base) IsExchangeSupported(exchange, address string) (ret bool) { + for x := range b.Addresses { + if b.Addresses[x].Address != address { continue } - exchangeList := strings.Split(Portfolio.Addresses[x].SupportedExchanges, ",") + exchangeList := strings.Split(b.Addresses[x].SupportedExchanges, ",") return common.StringDataContainsInsensitive(exchangeList, exchange) } return } // IsColdStorage checks if address is a cold storage wallet -func IsColdStorage(address string) (ret bool) { - for x := range Portfolio.Addresses { - if Portfolio.Addresses[x].Address != address { +func (b *Base) IsColdStorage(address string) bool { + for x := range b.Addresses { + if b.Addresses[x].Address != address { continue } - return Portfolio.Addresses[x].ColdStorage + return b.Addresses[x].ColdStorage } - return + return false } // IsWhiteListed checks if address is whitelisted for withdraw transfers -func IsWhiteListed(address string) (ret bool) { - for x := range Portfolio.Addresses { - if Portfolio.Addresses[x].Address != address { +func (b *Base) IsWhiteListed(address string) bool { + for x := range b.Addresses { + if b.Addresses[x].Address != address { continue } - return Portfolio.Addresses[x].WhiteListed + return b.Addresses[x].WhiteListed } - return + return false } diff --git a/portfolio/portfolio_test.go b/portfolio/portfolio_test.go index c2baac7a..73082f15 100644 --- a/portfolio/portfolio_test.go +++ b/portfolio/portfolio_test.go @@ -1,7 +1,7 @@ package portfolio import ( - "reflect" + "errors" "testing" "time" @@ -13,15 +13,12 @@ const ( testBTCAddress = "0x1D01TH0R53" ) -var ( - portfolioSeeded bool -) - func TestGetEthereumBalance(t *testing.T) { + b := Base{} address := "0xb794f5ea0ba39494ce839613fffba74279579268" nonsenseAddress := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - response, err := GetEthereumBalance(address) + response, err := b.GetEthereumBalance(address) if err != nil { t.Errorf("Portfolio GetEthereumBalance() Error: %s", err) } @@ -30,16 +27,16 @@ func TestGetEthereumBalance(t *testing.T) { t.Error("Portfolio GetEthereumBalance() address invalid") } - response, err = GetEthereumBalance(nonsenseAddress) - if response.Error.Message != "" || err == nil { - t.Errorf("Portfolio GetEthereumBalance() Error: %s", - response.Error.Message) + response, err = b.GetEthereumBalance(nonsenseAddress) + if !errors.Is(err, errNotEthAddress) { + t.Errorf("received '%v', expected '%v'", err, errNotEthAddress) } } func TestGetCryptoIDBalance(t *testing.T) { + b := Base{} ltcAddress := "LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1" - _, err := GetCryptoIDAddress(ltcAddress, currency.LTC) + _, err := b.GetCryptoIDAddress(ltcAddress, currency.LTC) if err != nil { t.Fatalf("TestGetCryptoIDBalance error: %s", err) } @@ -51,38 +48,42 @@ func TestGetAddressBalance(t *testing.T) { description := "Description of Wallet" balance := float64(1000) - portfolio := Base{} - portfolio.AddAddress(ltcAddress, description, ltc, balance) + b := Base{} + err := b.AddAddress(ltcAddress, description, ltc, balance) + if err != nil { + t.Error(err) + } - addBalance, _ := portfolio.GetAddressBalance("LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", + addBalance, _ := b.GetAddressBalance("LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", description, ltc) if addBalance != balance { - t.Error("Portfolio GetAddressBalance() Error: Incorrect value") + t.Error("Incorrect value") } - addBalance, found := portfolio.GetAddressBalance("WigWham", + addBalance, found := b.GetAddressBalance("WigWham", description, ltc) if addBalance != 0 { - t.Error("Portfolio GetAddressBalance() Error: Incorrect value") + t.Error("Incorrect value") } if found { - t.Error("Portfolio GetAddressBalance() Error: Incorrect value") + t.Error("Incorrect value") } } func TestGetRippleBalance(t *testing.T) { + b := Base{} nonsenseAddress := "Wigwham" - _, err := GetRippleBalance(nonsenseAddress) + _, err := b.GetRippleBalance(nonsenseAddress) if err == nil { t.Error("error cannot be nil on a bad address") } rippleAddress := "r962iS5subzbVeXZN8MTzyEuuaQKo5qksh" - _, err = GetRippleBalance(rippleAddress) + _, err = b.GetRippleBalance(rippleAddress) if err != nil { t.Error(err) } @@ -90,251 +91,278 @@ func TestGetRippleBalance(t *testing.T) { func TestExchangeExists(t *testing.T) { newBase := Base{} - newBase.AddAddress("someaddress", + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.02) + if err != nil { + t.Error(err) + } if !newBase.ExchangeExists("someaddress") { - t.Error("portfolio_test.go - AddressExists error") + t.Error("expected exchange to exist") } if newBase.ExchangeExists("bla") { - t.Error("portfolio_test.go - AddressExists error") + t.Error("expected exchange to not exist") } } func TestAddressExists(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", + newBase := Base{} + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.02) - - if !newbase.AddressExists("someaddress") { - t.Error("portfolio_test.go - AddressExists error") + if err != nil { + t.Error(err) } - if newbase.AddressExists("bla") { - t.Error("portfolio_test.go - AddressExists error") + + if !newBase.AddressExists("someaddress") { + t.Error("expected address to exist") + } + if newBase.AddressExists("bla") { + t.Error("expected address to not exist") } } func TestExchangeAddressExists(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", + newBase := Base{} + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) - - if !newbase.ExchangeAddressExists("someaddress", currency.LTC) { - t.Error("portfolio_test.go - ExchangeAddressExists error") + if err != nil { + t.Error(err) } - if newbase.ExchangeAddressExists("TEST", currency.LTC) { - t.Error("portfolio_test.go - ExchangeAddressExists error") + + if !newBase.ExchangeAddressExists("someaddress", currency.LTC) { + t.Error("expected exchange address to exist") + } + if newBase.ExchangeAddressExists("TEST", currency.LTC) { + t.Error("expected exchange address to not exist") } } func TestAddExchangeAddress(t *testing.T) { - newbase := Base{} - newbase.AddExchangeAddress("OKEX", currency.BTC, 100) - newbase.AddExchangeAddress("OKEX", currency.BTC, 200) + newBase := Base{} + newBase.AddExchangeAddress("OKEX", currency.BTC, 100) + newBase.AddExchangeAddress("OKEX", currency.BTC, 200) - if !newbase.ExchangeAddressExists("OKEX", currency.BTC) { - t.Error("TestExchangeAddressExists address doesn't exist") + if !newBase.ExchangeAddressExists("OKEX", currency.BTC) { + t.Error("address doesn't exist") } } func TestUpdateAddressBalance(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", + newBase := Base{} + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.02) + if err != nil { + t.Error(err) + } - newbase.UpdateAddressBalance("someaddress", 0.03) + newBase.UpdateAddressBalance("someaddress", 0.03) - value := newbase.GetPortfolioSummary() + value := newBase.GetPortfolioSummary() if value.Totals[0].Coin != currency.LTC && value.Totals[0].Balance != 0.03 { - t.Error("portfolio_test.go - UpdateUpdateAddressBalance error") + t.Error("UpdateUpdateAddressBalance error") } } func TestRemoveAddress(t *testing.T) { - var newbase Base - if err := newbase.RemoveAddress("", "MEOW", currency.LTC); err == nil { + var newBase Base + if err := newBase.RemoveAddress("", "MEOW", currency.LTC); err == nil { t.Error("invalid address should throw an error") } - if err := newbase.RemoveAddress("Gibson", "", currency.NewCode("")); err == nil { + if err := newBase.RemoveAddress("Gibson", "", currency.NewCode("")); err == nil { t.Error("invalid coin type should throw an error") } - if err := newbase.RemoveAddress("HIDDENERINO", "MEOW", currency.LTC); err == nil { + if err := newBase.RemoveAddress("HIDDENERINO", "MEOW", currency.LTC); err == nil { t.Error("non-existent address should throw an error") } - newbase.AddAddress("someaddr", + err := newBase.AddAddress("someaddr", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 420) - - if !newbase.AddressExists("someaddr") { - t.Error("portfolio_test.go - TestRemoveAddress") + if err != nil { + t.Error(err) } - newbase.RemoveAddress("someaddr", + if !newBase.AddressExists("someaddr") { + t.Error("address does not exist") + } + + err = newBase.RemoveAddress("someaddr", currency.LTC.String(), currency.NewCode("LTCWALLETTEST")) - if newbase.AddressExists("someaddr") { - t.Error("portfolio_test.go - TestRemoveAddress") + if err != nil { + t.Error(err) + } + if newBase.AddressExists("someaddr") { + t.Error("address should not exist") } } func TestRemoveExchangeAddress(t *testing.T) { - newbase := Base{} + newBase := Base{} exchangeName := "BallerExchange" coinType := currency.LTC - newbase.AddExchangeAddress(exchangeName, coinType, 420) + newBase.AddExchangeAddress(exchangeName, coinType, 420) - if !newbase.ExchangeAddressExists(exchangeName, coinType) { - t.Error("portfolio_test.go - TestRemoveAddress") + if !newBase.ExchangeAddressExists(exchangeName, coinType) { + t.Error("address does not exist") } - newbase.RemoveExchangeAddress(exchangeName, coinType) - if newbase.ExchangeAddressExists(exchangeName, coinType) { - t.Error("portfolio_test.go - TestRemoveAddress") + newBase.RemoveExchangeAddress(exchangeName, coinType) + if newBase.ExchangeAddressExists(exchangeName, coinType) { + t.Error("address should not exist") } } func TestUpdateExchangeAddressBalance(t *testing.T) { - newbase := Base{} - newbase.AddExchangeAddress("someaddress", currency.LTC, 0.02) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - portfolio.UpdateExchangeAddressBalance("someaddress", currency.LTC, 0.04) + newBase := Base{} + newBase.AddExchangeAddress("someaddress", currency.LTC, 0.02) + b := Base{} + b.Seed(newBase) + b.UpdateExchangeAddressBalance("someaddress", currency.LTC, 0.04) - value := portfolio.GetPortfolioSummary() + value := b.GetPortfolioSummary() if value.Totals[0].Coin != currency.LTC && value.Totals[0].Balance != 0.04 { - t.Error("portfolio_test.go - UpdateExchangeAddressBalance error") + t.Error("incorrect portfolio balance") } } func TestAddAddress(t *testing.T) { - var newbase Base - if err := newbase.AddAddress("", "MEOW", currency.LTC, 1); err == nil { + var newBase Base + if err := newBase.AddAddress("", "MEOW", currency.LTC, 1); err == nil { t.Error("invalid address should throw an error") } - if err := newbase.AddAddress("Gibson", "", currency.NewCode(""), 1); err == nil { + if err := newBase.AddAddress("Gibson", "", currency.NewCode(""), 1); err == nil { t.Error("invalid coin type should throw an error") } // test adding an exchange address - err := newbase.AddAddress("COINUT", PortfolioAddressExchange, currency.LTC, 0) + err := newBase.AddAddress("COINUT", ExchangeAddress, currency.LTC, 0) if err != nil { t.Errorf("failed to add address: %v", err) } // add a test portfolio address and amount - newbase.AddAddress("Gibson", + err = newBase.AddAddress("Gibson", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.02) + if err != nil { + t.Error(err) + } // test updating the balance and make sure it's reflected - newbase.AddAddress("Gibson", currency.LTC.String(), + err = newBase.AddAddress("Gibson", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.05) - b, _ := newbase.GetAddressBalance("Gibson", "LTC", + if err != nil { + t.Error(err) + } + b, _ := newBase.GetAddressBalance("Gibson", "LTC", currency.NewCode("LTCWALLETTEST")) if b != 0.05 { t.Error("invalid portfolio amount") } - portfolio := GetPortfolio() - portfolio.Seed(newbase) - if !portfolio.AddressExists("Gibson") { - t.Error("portfolio_test.go - AddAddress error") + nb := Base{} + nb.Seed(newBase) + if !nb.AddressExists("Gibson") { + t.Error("AddAddress error") } // Test updating balance to <= 0, expected result is to remove the address. // Fail if address still exists. - newbase.AddAddress("Gibson", + err = newBase.AddAddress("Gibson", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), -1) + if err != nil { + t.Error(err) + } - if newbase.AddressExists("Gibson") { - t.Error("portfolio_test.go - AddAddress error") + if newBase.AddressExists("Gibson") { + t.Error("AddAddress error") } } func TestUpdatePortfolio(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", + newBase := Base{} + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.02) + if err != nil { + t.Fatal(err) + } - portfolio := GetPortfolio() - portfolio.Seed(newbase) - - err := portfolio.UpdatePortfolio( + err = newBase.UpdatePortfolio( []string{"LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL"}, currency.LTC) if err != nil { - t.Error("portfolio_test.go - UpdatePortfolio error", err) + t.Error("UpdatePortfolio error", err) } - err = portfolio.UpdatePortfolio([]string{"Testy"}, currency.LTC) + err = newBase.UpdatePortfolio([]string{"Testy"}, currency.LTC) if err == nil { - t.Error("portfolio_test.go - UpdatePortfolio error cannot be nil") + t.Error("UpdatePortfolio error cannot be nil") } - err = portfolio.UpdatePortfolio([]string{ + err = newBase.UpdatePortfolio([]string{ "LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", "LVa8wZ983PvWtdwXZ8viK6SocMENLCXkEy"}, currency.LTC, ) if err != nil { - t.Error("portfolio_test.go - UpdatePortfolio error", err) + t.Error("UpdatePortfolio error", err) } - err = portfolio.UpdatePortfolio( + err = newBase.UpdatePortfolio( []string{"LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", "Testy"}, currency.LTC, ) if err == nil { - t.Error("portfolio_test.go - UpdatePortfolio error cannot be nil") + t.Error("UpdatePortfolio error cannot be nil") } time.Sleep(time.Second * 5) - err = portfolio.UpdatePortfolio([]string{ + err = newBase.UpdatePortfolio([]string{ "0xb794f5ea0ba39494ce839613fffba74279579268", "0xe853c56864a2ebe4576a807d26fdc4a0ada51919"}, currency.ETH) if err != nil { t.Error(err) } - err = portfolio.UpdatePortfolio([]string{ + err = newBase.UpdatePortfolio([]string{ "0xb794f5ea0ba39494ce839613fffba74279579268", "TESTY"}, currency.ETH) if err == nil { - t.Error("portfolio_test.go - UpdatePortfolio error cannot be nil") + t.Error("UpdatePortfolio error cannot be nil") } - err = portfolio.UpdatePortfolio([]string{PortfolioAddressExchange, - PortfolioAddressPersonal}, + err = newBase.UpdatePortfolio([]string{ExchangeAddress, + PersonalAddress}, currency.LTC) if err != nil { - t.Error("portfolio_test.go - UpdatePortfolio error", err) + t.Error(err) } - err = portfolio.UpdatePortfolio([]string{ + err = newBase.UpdatePortfolio([]string{ "r962iS5subzbVeXZN8MTzyEuuaQKo5qksh"}, currency.XRP) if err != nil { - t.Error("portfolio_test.go - UpdatePortfolio error", err) + t.Error(err) } - err = portfolio.UpdatePortfolio([]string{ + err = newBase.UpdatePortfolio([]string{ "r962iS5subzbVeXZN8MTzyEuuaQKo5qksh", "TESTY"}, currency.XRP) @@ -344,89 +372,119 @@ func TestUpdatePortfolio(t *testing.T) { } func TestGetPortfolioByExchange(t *testing.T) { - newbase := Base{} - newbase.AddExchangeAddress("OKEX", currency.LTC, 0.07) - newbase.AddExchangeAddress("Bitfinex", currency.LTC, 0.05) - newbase.AddAddress("someaddress", "LTC", currency.NewCode(PortfolioAddressPersonal), 0.03) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - value := portfolio.GetPortfolioByExchange("OKEX") + newBase := Base{} + newBase.AddExchangeAddress("OKEX", currency.LTC, 0.07) + newBase.AddExchangeAddress("Bitfinex", currency.LTC, 0.05) + err := newBase.AddAddress("someaddress", "LTC", currency.NewCode(PersonalAddress), 0.03) + if err != nil { + t.Fatal(err) + } + value := newBase.GetPortfolioByExchange("OKEX") result, ok := value[currency.LTC] if !ok { - t.Error("portfolio_test.go - GetPortfolioByExchange error") + t.Error("missing portfolio entry") } if result != 0.07 { - t.Error("portfolio_test.go - GetPortfolioByExchange result != 0.10") + t.Error("incorrect result") } - value = portfolio.GetPortfolioByExchange("Bitfinex") + value = newBase.GetPortfolioByExchange("Bitfinex") result, ok = value[currency.LTC] if !ok { - t.Error("portfolio_test.go - GetPortfolioByExchange error") + t.Error("missing portfolio entry") } if result != 0.05 { - t.Error("portfolio_test.go - GetPortfolioByExchange result != 0.05") + t.Error("incorrect result") } } func TestGetExchangePortfolio(t *testing.T) { - newbase := Base{} - newbase.AddAddress("OKEX", PortfolioAddressExchange, currency.LTC, 0.03) - newbase.AddAddress("Bitfinex", PortfolioAddressExchange, currency.LTC, 0.05) - newbase.AddAddress("someaddress", PortfolioAddressPersonal, currency.LTC, 0.03) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - value := portfolio.GetExchangePortfolio() + newBase := Base{} + err := newBase.AddAddress("OKEX", ExchangeAddress, currency.LTC, 0.03) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("Bitfinex", ExchangeAddress, currency.LTC, 0.05) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("someaddress", PersonalAddress, currency.LTC, 0.03) + if err != nil { + t.Fatal(err) + } + + value := newBase.GetExchangePortfolio() result, ok := value[currency.LTC] if !ok { - t.Error("portfolio_test.go - GetExchangePortfolio error") + t.Error("missing portfolio entry") } if result != 0.08 { - t.Error("portfolio_test.go - GetExchangePortfolio result != 0.08") + t.Error("result != 0.08") } } func TestGetPersonalPortfolio(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", PortfolioAddressPersonal, currency.N2O, 0.02) - newbase.AddAddress("anotheraddress", PortfolioAddressPersonal, currency.N2O, 0.03) - newbase.AddAddress("Exchange", PortfolioAddressExchange, currency.N2O, 0.01) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - value := portfolio.GetPersonalPortfolio() + newBase := Base{} + err := newBase.AddAddress("someaddress", PersonalAddress, currency.N2O, 0.02) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("anotheraddress", PersonalAddress, currency.N2O, 0.03) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("Exchange", ExchangeAddress, currency.N2O, 0.01) + if err != nil { + t.Fatal(err) + } + + value := newBase.GetPersonalPortfolio() result, ok := value[currency.N2O] if !ok { - t.Error("portfolio_test.go - GetPersonalPortfolio error") + t.Error("GetPersonalPortfolio error") } if result != 0.05 { - t.Error("portfolio_test.go - GetPersonalPortfolio result != 0.05") + t.Error("GetPersonalPortfolio result != 0.05") } } func TestGetPortfolioSummary(t *testing.T) { - newbase := Base{} + newBase := Base{} // Personal holdings - newbase.AddAddress("someaddress", PortfolioAddressPersonal, currency.LTC, 1) - newbase.AddAddress("someaddress2", PortfolioAddressPersonal, currency.LTC, 2) - newbase.AddAddress("someaddress3", PortfolioAddressPersonal, currency.BTC, 100) - newbase.AddAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", - PortfolioAddressPersonal, currency.ETH, 865346880000000000) - newbase.AddAddress("0x9edc81c813b26165f607a8d1b8db87a02f34307f", - PortfolioAddressPersonal, currency.ETH, 165346880000000000) + err := newBase.AddAddress("someaddress", PersonalAddress, currency.LTC, 1) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("someaddress2", PersonalAddress, currency.LTC, 2) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("someaddress3", PersonalAddress, currency.BTC, 100) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + PersonalAddress, currency.ETH, 865346880000000000) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("0x9edc81c813b26165f607a8d1b8db87a02f34307f", + PersonalAddress, currency.ETH, 165346880000000000) + if err != nil { + t.Fatal(err) + } // Exchange holdings - newbase.AddExchangeAddress("Bitfinex", currency.LTC, 20) - newbase.AddExchangeAddress("Bitfinex", currency.BTC, 100) - newbase.AddExchangeAddress("OKEX", currency.ETH, 42) + newBase.AddExchangeAddress("Bitfinex", currency.LTC, 20) + newBase.AddExchangeAddress("Bitfinex", currency.BTC, 100) + newBase.AddExchangeAddress("OKEX", currency.ETH, 42) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - value := portfolio.GetPortfolioSummary() + value := newBase.GetPortfolioSummary() getTotalsVal := func(c currency.Code) Coin { for x := range value.Totals { @@ -438,84 +496,89 @@ func TestGetPortfolioSummary(t *testing.T) { } if getTotalsVal(currency.LTC).Coin != currency.LTC { - t.Error("portfolio_test.go - TestGetPortfolioSummary error") + t.Error("mismatched currency") } if getTotalsVal(currency.ETH).Coin == currency.LTC { - t.Error("portfolio_test.go - TestGetPortfolioSummary error") + t.Error("mismatched currency") } if getTotalsVal(currency.LTC).Balance != 23 { - t.Error("portfolio_test.go - TestGetPortfolioSummary error") + t.Error("incorrect balance") } if getTotalsVal(currency.BTC).Balance != 200 { - t.Error("portfolio_test.go - TestGetPortfolioSummary error") + t.Error("incorrect balance") } } func TestGetPortfolioGroupedCoin(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) - newbase.AddAddress("Exchange", PortfolioAddressExchange, currency.LTC, 0.05) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - value := portfolio.GetPortfolioGroupedCoin() + newBase := Base{} + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) + if err != nil { + t.Fatal(err) + } + err = newBase.AddAddress("Exchange", ExchangeAddress, currency.LTC, 0.05) + if err != nil { + t.Fatal(err) + } + + value := newBase.GetPortfolioGroupedCoin() if value[currency.LTC][0] != "someaddress" && len(value[currency.LTC][0]) != 1 { - t.Error("portfolio_test.go - GetPortfolioGroupedCoin error") + t.Error("incorrect balance") } } func TestSeed(t *testing.T) { - newbase := Base{} - newbase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) - portfolio := GetPortfolio() - portfolio.Seed(newbase) - - if !portfolio.AddressExists("someaddress") { - t.Error("portfolio_test.go - Seed error") + newBase := Base{} + err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) + if err != nil { + t.Fatal(err) + } + if !newBase.AddressExists("someaddress") { + t.Error("Seed error") } } func TestIsExchangeSupported(t *testing.T) { - seedPortFolioForTest(t) - ret := IsExchangeSupported("BTC Markets", core.BitcoinDonationAddress) + newBase := seedPortFolioForTest(t) + ret := newBase.IsExchangeSupported("BTC Markets", core.BitcoinDonationAddress) if !ret { t.Fatal("expected IsExchangeSupported() to return true") } - ret = IsExchangeSupported("Kraken", core.BitcoinDonationAddress) + ret = newBase.IsExchangeSupported("Kraken", core.BitcoinDonationAddress) if ret { t.Fatal("expected IsExchangeSupported() to return false") } } func TestIsColdStorage(t *testing.T) { - seedPortFolioForTest(t) - ret := IsColdStorage(core.BitcoinDonationAddress) + newBase := seedPortFolioForTest(t) + ret := newBase.IsColdStorage(core.BitcoinDonationAddress) if !ret { t.Fatal("expected IsColdStorage() to return true") } - ret = IsColdStorage(testBTCAddress) + ret = newBase.IsColdStorage(testBTCAddress) if ret { t.Fatal("expected IsColdStorage() to return false") } - ret = IsColdStorage("hello") + ret = newBase.IsColdStorage("hello") if ret { t.Fatal("expected IsColdStorage() to return false") } } func TestIsWhiteListed(t *testing.T) { - seedPortFolioForTest(t) - ret := IsWhiteListed(core.BitcoinDonationAddress) + b := seedPortFolioForTest(t) + ret := b.IsWhiteListed(core.BitcoinDonationAddress) if !ret { t.Fatal("expected IsWhiteListed() to return true") } - ret = IsWhiteListed(testBTCAddress) + ret = b.IsWhiteListed(testBTCAddress) if ret { t.Fatal("expected IsWhiteListed() to return false") } - ret = IsWhiteListed("hello") + ret = b.IsWhiteListed("hello") if ret { t.Fatal("expected IsWhiteListed() to return false") } @@ -523,38 +586,31 @@ func TestIsWhiteListed(t *testing.T) { func TestStartPortfolioWatcher(t *testing.T) { newBase := Base{} - newBase.AddAddress("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1", + err := newBase.AddAddress("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1", currency.LTC.String(), - currency.NewCode(PortfolioAddressPersonal), + currency.NewCode(PersonalAddress), 0.02) - - newBase.AddAddress("Testy", - currency.LTC.String(), - currency.NewCode(PortfolioAddressPersonal), - 0.02) - - portfolio := GetPortfolio() - portfolio.Seed(newBase) - - if !portfolio.AddressExists("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1") { - t.Error("portfolio_test.go - TestStartPortfolioWatcher") + if err != nil { + t.Error(err) } - go StartPortfolioWatcher() -} - -func TestGetPortfolio(t *testing.T) { - ptrBASE := GetPortfolio() - if reflect.TypeOf(ptrBASE).String() != "*portfolio.Base" { - t.Error("portfolio_test.go - GetoPortfolio error") + err = newBase.AddAddress("Testy", + currency.LTC.String(), + currency.NewCode(PersonalAddress), + 0.02) + if err != nil { + t.Error(err) } + + if !newBase.AddressExists("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1") { + t.Error("address does not exist") + } + + go newBase.StartPortfolioWatcher() } -func seedPortFolioForTest(t *testing.T) { +func seedPortFolioForTest(t *testing.T) *Base { t.Helper() - if portfolioSeeded { - return - } newBase := Base{} err := newBase.AddAddress(core.BitcoinDonationAddress, "test", currency.BTC, 1500) @@ -570,6 +626,10 @@ func seedPortFolioForTest(t *testing.T) { t.Fatalf("failed to add portfolio address with reason: %v, unable to continue tests", err) } newBase.Addresses[1].SupportedExchanges = "BTC Markets,Binance" - portfolio := GetPortfolio() - portfolio.Seed(newBase) + b := Base{} + b.Seed(newBase) + if len(b.Addresses) == 0 { + t.Error("failed to seed") + } + return &b } diff --git a/portfolio/portfolio_types.go b/portfolio/portfolio_types.go index 8c46e3d3..1694252d 100644 --- a/portfolio/portfolio_types.go +++ b/portfolio/portfolio_types.go @@ -9,6 +9,7 @@ import ( // Base holds the portfolio base addresses type Base struct { Addresses []Address `json:"addresses"` + Verbose bool } // Address sub type holding address information for portfolio diff --git a/portfolio/withdraw/validate.go b/portfolio/withdraw/validate.go index fc694e9c..99f62463 100644 --- a/portfolio/withdraw/validate.go +++ b/portfolio/withdraw/validate.go @@ -6,7 +6,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" - "github.com/thrasher-corp/gocryptotrader/portfolio" ) // Validate takes interface and passes to asset type to check the request meets requirements to submit @@ -33,12 +32,12 @@ func (r *Request) Validate(opt ...validate.Checker) (err error) { if (r.Currency != currency.Code{}) && !r.Currency.IsFiatCurrency() { allErrors = append(allErrors, ErrStrCurrencyNotFiat) } - allErrors = append(allErrors, validateFiat(r)...) + allErrors = append(allErrors, r.validateFiat()...) case Crypto: if (r.Currency != currency.Code{}) && !r.Currency.IsCryptocurrency() { allErrors = append(allErrors, ErrStrCurrencyNotCrypto) } - allErrors = append(allErrors, validateCrypto(r)...) + allErrors = append(allErrors, r.validateCrypto()...) default: allErrors = append(allErrors, "invalid request type") } @@ -60,30 +59,25 @@ func (r *Request) Validate(opt ...validate.Checker) (err error) { } // validateFiat takes interface and passes to asset type to check the request meets requirements to submit -func validateFiat(request *Request) (err []string) { - errBank := request.Fiat.Bank.ValidateForWithdrawal(request.Exchange, request.Currency) +func (r *Request) validateFiat() []string { + var resp []string + errBank := r.Fiat.Bank.ValidateForWithdrawal(r.Exchange, r.Currency) if errBank != nil { - err = append(err, errBank...) + resp = append(resp, errBank...) } - return err + return resp } // validateCrypto checks if Crypto request is valid and meets the minimum requirements to submit a crypto withdrawal request -func validateCrypto(request *Request) (err []string) { - if !portfolio.IsWhiteListed(request.Crypto.Address) { - err = append(err, ErrStrAddressNotWhiteListed) +func (r *Request) validateCrypto() []string { + var resp []string + + if r.Crypto.Address == "" { + resp = append(resp, ErrStrAddressNotSet) } - if !portfolio.IsExchangeSupported(request.Exchange, request.Crypto.Address) { - err = append(err, ErrStrExchangeNotSupportedByAddress) + if r.Crypto.FeeAmount < 0 { + resp = append(resp, ErrStrFeeCannotBeNegative) } - - if request.Crypto.Address == "" { - err = append(err, ErrStrAddressNotSet) - } - - if request.Crypto.FeeAmount < 0 { - err = append(err, ErrStrFeeCannotBeNegative) - } - return + return resp } diff --git a/portfolio/withdraw/validate_test.go b/portfolio/withdraw/validate_test.go index 07c2194d..00aedc89 100644 --- a/portfolio/withdraw/validate_test.go +++ b/portfolio/withdraw/validate_test.go @@ -112,21 +112,22 @@ var ( ) func TestMain(m *testing.M) { - err := portfolio.Portfolio.AddAddress(core.BitcoinDonationAddress, "test", currency.BTC, 1500) + var p portfolio.Base + err := p.AddAddress(core.BitcoinDonationAddress, "test", currency.BTC, 1500) if err != nil { fmt.Printf("failed to add portfolio address with reason: %v, unable to continue tests", err) os.Exit(0) } - portfolio.Portfolio.Addresses[0].WhiteListed = true - portfolio.Portfolio.Addresses[0].ColdStorage = true - portfolio.Portfolio.Addresses[0].SupportedExchanges = "BTC Markets,Binance" + p.Addresses[0].WhiteListed = true + p.Addresses[0].ColdStorage = true + p.Addresses[0].SupportedExchanges = "BTC Markets,Binance" - err = portfolio.Portfolio.AddAddress(testBTCAddress, "test", currency.BTC, 1500) + err = p.AddAddress(testBTCAddress, "test", currency.BTC, 1500) if err != nil { fmt.Printf("failed to add portfolio address with reason: %v, unable to continue tests", err) os.Exit(0) } - portfolio.Portfolio.Addresses[1].SupportedExchanges = "BTC Markets,Binance" + p.Addresses[1].SupportedExchanges = "BTC Markets,Binance" banking.Accounts = append(banking.Accounts, banking.Account{ @@ -286,9 +287,7 @@ func TestValidateCrypto(t *testing.T) { { "Invalid-Nil", invalidCryptoNilRequest, - errors.New(ErrStrAddressNotWhiteListed + ", " + - ErrStrExchangeNotSupportedByAddress + ", " + - ErrStrAddressNotSet), + errors.New(ErrStrAddressNotSet), }, { "NoRequest", @@ -304,15 +303,9 @@ func TestValidateCrypto(t *testing.T) { { "NoAddress", invalidCryptoNoAddressRequest, - errors.New(ErrStrAddressNotWhiteListed + ", " + - ErrStrExchangeNotSupportedByAddress + ", " + + errors.New( ErrStrAddressNotSet), }, - { - "NonWhiteListed", - invalidCryptoNonWhiteListedAddressRequest, - errors.New(ErrStrAddressNotWhiteListed), - }, { "NegativeFee", invalidCryptoNegativeFeeRequest, diff --git a/portfolio/withdraw/withdraw_types.go b/portfolio/withdraw/withdraw_types.go index c838ab00..b2e5fd4a 100644 --- a/portfolio/withdraw/withdraw_types.go +++ b/portfolio/withdraw/withdraw_types.go @@ -38,9 +38,7 @@ const ( // ErrStrFeeCannotBeNegative message to return when fee amount is negative ErrStrFeeCannotBeNegative = "fee amount cannot be negative" // ErrStrAddressNotWhiteListed message to return when attempting to withdraw to non-whitelisted address - ErrStrAddressNotWhiteListed = "address is not whitelisted for withdrawals" - // ErrStrExchangeNotSupportedByAddress message to return when attemptign to withdraw to an unsupported exchange - ErrStrExchangeNotSupportedByAddress = "address is not supported by exchange" + ) var ( @@ -50,6 +48,10 @@ var ( ErrExchangeNameUnset = errors.New("exchange name unset") // ErrInvalidRequest message to return when a request type is invalid ErrInvalidRequest = errors.New("invalid request type") + // ErrStrAddressNotWhiteListed occurs when a withdrawal attempts to withdraw from a non-whitelisted address + ErrStrAddressNotWhiteListed = errors.New("address is not whitelisted for withdrawals") + // ErrStrExchangeNotSupportedByAddress message to return when attemptign to withdraw to an unsupported exchange + ErrStrExchangeNotSupportedByAddress = errors.New("address is not supported by exchange") // CacheSize cache size to use for withdrawal request history CacheSize uint64 = 25 // Cache LRU cache for recent requests diff --git a/testdata/README.md b/testdata/README.md index b265af32..42a52894 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Testdata - + [![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) diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index ec44000a..65eda425 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -309928,7 +309928,7 @@ "success": true, "id": "7213fea8e94b4a5593d507237e5a555b" }, - "queryString": "address=bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc\u0026amount=0\u0026asset=BTC\u0026name=WITHDRAW+IT+ALL\u0026recvWindow=5000\u0026signature=bec597908e6d2c223790cac9ca46300109216e056690a3f10a279b9eecc97e7e\u0026timestamp=1560233386000", + "queryString": "address=bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc\u0026amount=0.00001337\u0026asset=BTC\u0026name=WITHDRAW+IT+ALL\u0026recvWindow=5000\u0026signature=bec597908e6d2c223790cac9ca46300109216e056690a3f10a279b9eecc97e7e\u0026timestamp=1560233386000", "bodyParams": "", "headers": { "Key": [ diff --git a/testdata/http_mock/poloniex/poloniex.json b/testdata/http_mock/poloniex/poloniex.json index 84610822..519af8a4 100644 --- a/testdata/http_mock/poloniex/poloniex.json +++ b/testdata/http_mock/poloniex/poloniex.json @@ -9754,7 +9754,7 @@ "response": "Withdrew 0.0 LTC." }, "queryString": "", - "bodyParams": "address=bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc\u0026amount=0\u0026command=withdraw\u0026currency=LTC\u0026nonce=1594157624217368003", + "bodyParams": "address=bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc\u0026amount=0.00001337\u0026command=withdraw\u0026currency=LTC\u0026nonce=1594157624217368003", "headers": { "Content-Type": [ "application/x-www-form-urlencoded" diff --git a/web/README.md b/web/README.md index 55da55ff..2af3545d 100644 --- a/web/README.md +++ b/web/README.md @@ -1,6 +1,6 @@ # GoCryptoTrader package Web - + [![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)