diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 907db755..1bd5d584 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -2,8 +2,8 @@ Thanks to the following contributors: thrasher- | https://github.com/thrasher- shazbert | https://github.com/shazbert -gloriousCode | https://github.com/gloriousCode dependabot[bot] | https://github.com/apps/dependabot +gloriousCode | https://github.com/gloriousCode dependabot-preview[bot] | https://github.com/apps/dependabot-preview xtda | https://github.com/xtda gbjk | https://github.com/gbjk @@ -13,13 +13,13 @@ vazha | https://github.com/vazha ydm | https://github.com/ydm ermalguni | https://github.com/ermalguni MadCozBadd | https://github.com/MadCozBadd +Beadko | https://github.com/Beadko vadimzhukck | https://github.com/vadimzhukck 140am | https://github.com/140am marcofranssen | https://github.com/marcofranssen geseq | https://github.com/geseq -Beadko | https://github.com/Beadko -TaltaM | https://github.com/TaltaM samuael | https://github.com/samuael +TaltaM | https://github.com/TaltaM dackroyd | https://github.com/dackroyd cranktakular | https://github.com/cranktakular khcchiu | https://github.com/khcchiu diff --git a/README.md b/README.md index 68c8bc5c..9a53d1cb 100644 --- a/README.md +++ b/README.md @@ -144,25 +144,25 @@ Binaries will be published once the codebase reaches a stable condition. |User|Contribution Amount| |--|--| | [thrasher-](https://github.com/thrasher-) | 683 | -| [shazbert](https://github.com/shazbert) | 301 | -| [gloriousCode](https://github.com/gloriousCode) | 219 | -| [dependabot[bot]](https://github.com/apps/dependabot) | 207 | +| [shazbert](https://github.com/shazbert) | 313 | +| [dependabot[bot]](https://github.com/apps/dependabot) | 227 | +| [gloriousCode](https://github.com/gloriousCode) | 224 | | [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 | | [xtda](https://github.com/xtda) | 47 | -| [gbjk](https://github.com/gbjk) | 35 | +| [gbjk](https://github.com/gbjk) | 40 | | [lrascao](https://github.com/lrascao) | 27 | | [Rots](https://github.com/Rots) | 15 | | [vazha](https://github.com/vazha) | 15 | | [ydm](https://github.com/ydm) | 15 | | [ermalguni](https://github.com/ermalguni) | 14 | | [MadCozBadd](https://github.com/MadCozBadd) | 13 | +| [Beadko](https://github.com/Beadko) | 10 | | [vadimzhukck](https://github.com/vadimzhukck) | 10 | | [140am](https://github.com/140am) | 8 | | [marcofranssen](https://github.com/marcofranssen) | 8 | | [geseq](https://github.com/geseq) | 8 | -| [Beadko](https://github.com/Beadko) | 6 | +| [samuael](https://github.com/samuael) | 7 | | [TaltaM](https://github.com/TaltaM) | 6 | -| [samuael](https://github.com/samuael) | 6 | | [dackroyd](https://github.com/dackroyd) | 5 | | [cranktakular](https://github.com/cranktakular) | 5 | | [khcchiu](https://github.com/khcchiu) | 5 | diff --git a/cmd/documentation/currency_templates/fx.tmpl b/cmd/documentation/currency_templates/fx.tmpl index 14a0fba9..6e9e55f1 100644 --- a/cmd/documentation/currency_templates/fx.tmpl +++ b/cmd/documentation/currency_templates/fx.tmpl @@ -7,7 +7,6 @@ + Exchange Rates support + Fixer.io support + Open Exchange Rates support -+ ExchangeRate.host support ### Please click GoDocs chevron above to view current GoDoc information for this package {{template "contributions"}} diff --git a/cmd/documentation/currency_templates/fx_exchangeratehost.tmpl b/cmd/documentation/currency_templates/fx_exchangeratehost.tmpl deleted file mode 100644 index 24a9c900..00000000 --- a/cmd/documentation/currency_templates/fx_exchangeratehost.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{define "currency forexprovider exchangerate.host" -}} -{{template "header" .}} -## Current Features for {{.Name}} - -+ Fetches up to date currency data from [ExchangeRate.host API]("https://exchangerate.host") - -### How to enable - -+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example) - -+ Individual package example below: -```go -import ( - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host" -) - -var c exchangeratehost.ExchangeRateHost - -// Define configuration -newSettings := base.Settings{ - Name: "ExchangeRateHost", - // ... -} - -c.Setup(newSettings) - -rates, err := c.GetRates("USD", "EUR,AUD") -// Handle error -``` - -### Please click GoDocs chevron above to view current GoDoc information for this package -{{template "contributions"}} -{{template "donations" .}} -{{- end}} \ No newline at end of file diff --git a/config/config_types.go b/config/config_types.go index 657cabbe..c69a942e 100644 --- a/config/config_types.go +++ b/config/config_types.go @@ -68,12 +68,11 @@ const ( // Constants here define unset default values displayed in the config.json // file const ( - APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" - WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - DefaultUnsetAPIKey = "Key" - DefaultUnsetAPISecret = "Secret" - DefaultUnsetAccountPlan = "accountPlan" - DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost" + APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" + WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + DefaultUnsetAPIKey = "Key" + DefaultUnsetAPISecret = "Secret" + DefaultUnsetAccountPlan = "accountPlan" ) // Variables here are used for configuration diff --git a/currency/conversion_test.go b/currency/conversion_test.go index 189b18f2..ca97648d 100644 --- a/currency/conversion_test.go +++ b/currency/conversion_test.go @@ -2,32 +2,25 @@ package currency import ( "fmt" - "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestNewConversionFromString(t *testing.T) { - expected := "AUDUSD" - conv, err := NewConversionFromString(expected) - if err != nil { - t.Error(err) - } - if conv.String() != expected { - t.Errorf("NewConversion() error expected %s but received %s", - expected, - conv) - } + conv, err := NewConversionFromString("AUDUSD") + assert.NoError(t, err, "NewConversionFromString should not error") + assert.Equal(t, "AUDUSD", conv.String(), "Should provide correct conversion currency") + r, err := conv.GetRate() + assert.NoError(t, err, "GetRate should not error") + assert.Positive(t, r, "Should provide correct conversion rate") - newexpected := strings.ToLower(expected) - conv, err = NewConversionFromString(newexpected) - if err != nil { - t.Error(err) - } - if conv.String() != newexpected { - t.Errorf("NewConversion() error expected %s but received %s", - newexpected, - conv) - } + conv, err = NewConversionFromString("audusd") + assert.NoError(t, err, "NewConversionFromString should not error") + assert.Equal(t, "audusd", conv.String(), "Should provide correct conversion for lowercase") + r, err = conv.GetRate() + assert.NoError(t, err, "GetRate should not error") + assert.Positive(t, r, "Should provide correct conversion rate") } func TestNewConversionFromStrings(t *testing.T) { diff --git a/currency/currency_types.go b/currency/currency_types.go index de9d1aa4..97575ed8 100644 --- a/currency/currency_types.go +++ b/currency/currency_types.go @@ -34,7 +34,6 @@ type BotOverrides struct { ExchangeRates bool Fixer bool OpenExchangeRates bool - ExchangeRateHost bool } // CoinmarketcapSettings refers to settings diff --git a/currency/forexprovider/README.md b/currency/forexprovider/README.md index 25118167..eca205a7 100644 --- a/currency/forexprovider/README.md +++ b/currency/forexprovider/README.md @@ -25,7 +25,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + Exchange Rates support + Fixer.io support + Open Exchange Rates support -+ ExchangeRate.host support ### Please click GoDocs chevron above to view current GoDoc information for this package diff --git a/currency/forexprovider/currencylayer/currencylayer_test.go b/currency/forexprovider/currencylayer/currencylayer_test.go index 1105a306..bbcb1f40 100644 --- a/currency/forexprovider/currencylayer/currencylayer_test.go +++ b/currency/forexprovider/currencylayer/currencylayer_test.go @@ -1,137 +1,129 @@ package currencylayer import ( + "log" + "os" + "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" ) var c CurrencyLayer -// please set your API key here for due diligence testing NOTE be aware you will -// minimize your API calls using this test. -const ( - APIkey = "" - Apilevel = 0 +// Either set your API key here or in env var for integration testing +var ( + apiKey = "" + apiKeyLevel = 0 ) var isSet bool -func setup() error { - if !isSet { - defaultCfg := base.Settings{ - Name: "CurrencyLayer", - Enabled: true, - } - - if APIkey != "" { - defaultCfg.APIKey = APIkey - } - - if Apilevel > -2 && Apilevel < 4 { - defaultCfg.APIKeyLvl = Apilevel - } - - err := c.Setup(defaultCfg) - if err != nil { - return err - } - isSet = true +func TestMain(m *testing.M) { + if apiKey == "" { + apiKey = os.Getenv("CURRENCYLAYER_APIKEY") } - return nil + + cfg := base.Settings{ + Name: "CurrencyLayer", + Enabled: true, + APIKeyLvl: apiKeyLevel, + APIKey: apiKey, + } + + if err := c.Setup(cfg); err != nil { + log.Fatal(err) + } + os.Exit(m.Run()) } -func areAPIKeysSet() bool { - return APIkey != "" && Apilevel != -1 +func skipIfNoAPIKey(tb testing.TB) { + tb.Helper() + if apiKey == "" { + tb.Skip("No API Key configured. Set env var CURRENCYLAYER_APIKEY") + } } -func TestGetRates(t *testing.T) { - err := setup() - if err != nil { - t.Skip("CurrencyLayer GetRates error", err) - } - _, err = c.GetRates("USD", "AUD") - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetRates() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetRates() error cannot be nil") - } +func TestNoAPIKey(t *testing.T) { + t.Parallel() + n := c + n.APIKey = "" + _, err := n.GetSupportedCurrencies() + assert.ErrorContains(t, err, "You have not supplied an API Access Key", "Should error APIKeyRequired") } func TestGetSupportedCurrencies(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer GetSupportedCurrencies error", err) + t.Parallel() + skipIfNoAPIKey(t) + currs, err := c.GetSupportedCurrencies() + if assert.NoError(t, err, "GetSupportedCurrencies should not error") { + assert.Contains(t, currs, "AUD", "AUD is a valid currency") + assert.Contains(t, currs, "USD", "USD is a valid currency") // Might fail in the near future } - _, err = c.GetSupportedCurrencies() - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetSupportedCurrencies() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetSupportedCurrencies() error cannot be nil") +} + +func TestGetRates(t *testing.T) { + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.GetRates("USD", "AUD") + if assert.NoError(t, err, "GetRates should not error") { + assert.Contains(t, r, "USDAUD", "Should find a USDAUD rate") + assert.Positive(t, r["USDAUD"], "Rate should be positive") } } func TestGetliveData(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer GetliveData error", err) - } - _, err = c.GetliveData("AUD", "USD") - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetliveData() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetliveData() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.GetliveData("EUR", "GBP") + if assert.NoError(t, err, "GetliveData should not error") { + assert.Contains(t, r, "GBPEUR", "Should find rate") + assert.Positive(t, r["GBPEUR"], "Rate should be positive") } } func TestGetHistoricalData(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer GetHistoricalData error", err) - } - _, err = c.GetHistoricalData("2016-12-15", []string{"AUD"}, "USD") - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetHistoricalData() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetHistoricalData() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.GetHistoricalData("2022-09-26", []string{"USD"}, "EUR") + if assert.NoError(t, err, "GetHistoricalData should not error") { + assert.Contains(t, r, "EURUSD", "Should find rate") + assert.Equal(t, r["EURUSD"], 0.962232, "Rate should be exactly correct") } } func TestConvert(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer Convert error", err) - } - _, err = c.Convert("USD", "AUD", "", 1) - if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountBasic { - t.Error("test error - currencylayer Convert() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer Convert() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.Convert("USD", "AUD", "", 1) + if assert.NoError(t, err, "Convert should not error") { + assert.Positive(t, r, "Should get a positive rate") } } func TestQueryTimeFrame(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer QueryTimeFrame error", err) - } - _, err = c.QueryTimeFrame("2010-12-0", "2010-12-5", "USD", []string{"AUD"}) - if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountPro { - t.Error("test error - currencylayer QueryTimeFrame() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer QueryTimeFrame() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.QueryTimeFrame("2020-03-12", "2020-03-16", "USD", []string{"AUD"}) + if assert.NoError(t, err, "QueryTimeFrame should not error") { + assert.Len(t, r, 5, "Should get correct number of days") + a, ok := r["2020-03-16"].(map[string]any) + assert.True(t, ok, "Has final date entry") + assert.Equal(t, a["USDAUD"], 1.6397, "And it was a bad week") } } func TestQueryCurrencyChange(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer QueryCurrencyChange() error", err) - } - _, err = c.QueryCurrencyChange("2010-12-0", "2010-12-5", "USD", []string{"AUD"}) - if areAPIKeysSet() && err != nil && c.APIKeyLvl == AccountEnterprise { - t.Error("test error - currencylayer QueryCurrencyChange() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer QueryCurrencyChange() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.QueryCurrencyChange("2030-03-12", "2030-03-16", "USD", []string{"AUD"}) + switch { + case err != nil && strings.Contains(err.Error(), "insufficient API privileges, upgrade to basic to use this function"): + t.Skip("Upgrade to Basic API plan to test Currency Change") + case assert.NoError(t, err, "QueryCurrencyChange should not error"): + assert.Contains(t, r, "USDAUD", "Should find change") + assert.Positive(t, r["USDAUD"].Change, "Change should be positive") + assert.Positive(t, r["USDAUD"].ChangePCT, "Change PCT should be positive") } } diff --git a/currency/forexprovider/currencylayer/currencylayer_types.go b/currency/forexprovider/currencylayer/currencylayer_types.go index 43848c7b..6d6351fa 100644 --- a/currency/forexprovider/currencylayer/currencylayer_types.go +++ b/currency/forexprovider/currencylayer/currencylayer_types.go @@ -33,6 +33,7 @@ type CurrencyLayer struct { // Error Defines the response error if an error occurred type Error struct { Code int `json:"code"` + Type string `json:"type"` Info string `json:"info"` } diff --git a/currency/forexprovider/exchangerate.host/README.md b/currency/forexprovider/exchangerate.host/README.md deleted file mode 100644 index 114f5909..00000000 --- a/currency/forexprovider/exchangerate.host/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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) -[![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/currency/forexprovider/exchangerate.host) -[![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 exchangerate.host 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 exchangerate.host - -+ Fetches up to date currency data from [ExchangeRate.host API]("https://exchangerate.host") - -### How to enable - -+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example) - -+ Individual package example below: -```go -import ( - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host" -) - -var c exchangeratehost.ExchangeRateHost - -// Define configuration -newSettings := base.Settings{ - Name: "ExchangeRateHost", - // ... -} - -c.Setup(newSettings) - -rates, err := c.GetRates("USD", "EUR,AUD") -// Handle error -``` - -### 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*** \ No newline at end of file diff --git a/currency/forexprovider/exchangerate.host/exchangerate.go b/currency/forexprovider/exchangerate.host/exchangerate.go deleted file mode 100644 index 420b1213..00000000 --- a/currency/forexprovider/exchangerate.host/exchangerate.go +++ /dev/null @@ -1,265 +0,0 @@ -package exchangeratehost - -import ( - "context" - "errors" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" -) - -// A client for the exchangerate.host API. NOTE: The format and callback -// parameters aren't supported as they're not needed for this implementation. -// Furthermore, the source is set to "ECB" as default - -const ( - timeLayout = "2006-01-02" - exchangeRateHostURL = "https://api.exchangerate.host" -) - -var ( - // DefaultSource uses the ecb for forex rates - DefaultSource = "ecb" -) - -// Setup sets up the ExchangeRateHost config -func (e *ExchangeRateHost) Setup(config base.Settings) error { - e.Name = config.Name - e.Enabled = config.Enabled - e.Verbose = config.Verbose - e.PrimaryProvider = config.PrimaryProvider - var err error - e.Requester, err = request.New(e.Name, - common.NewHTTPClientWithTimeout(base.DefaultTimeOut)) - return err -} - -// GetLatestRates returns a list of forex rates based on the supplied params -func (e *ExchangeRateHost) GetLatestRates(baseCurrency, symbols string, amount float64, places int64, source string) (*LatestRates, error) { - v := url.Values{} - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var l LatestRates - return &l, e.SendHTTPRequest("latest", v, &l) -} - -// ConvertCurrency converts a currency based on the supplied params -func (e *ExchangeRateHost) ConvertCurrency(from, to, baseCurrency, symbols, source string, date time.Time, amount float64, places int64) (*ConvertCurrency, error) { - v := url.Values{} - if from != "" { - v.Set("from", from) - } - if to != "" { - v.Set("to", to) - } - if !date.IsZero() { - v.Set("date", date.UTC().Format(timeLayout)) - } - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - if symbols != "" { - v.Set("symbols", symbols) - } - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var c ConvertCurrency - return &c, e.SendHTTPRequest("convert", v, &c) -} - -// GetHistoricalRates returns a list of historical rates based on the supplied params -func (e *ExchangeRateHost) GetHistoricalRates(date time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*HistoricRates, error) { - v := url.Values{} - if date.IsZero() { - date = time.Now() - } - fmtDate := date.UTC().Format(timeLayout) - v.Set("date", fmtDate) - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var h HistoricRates - return &h, e.SendHTTPRequest(fmtDate, v, &h) -} - -// GetTimeSeries returns time series forex data based on the supplied params -func (e *ExchangeRateHost) GetTimeSeries(startDate, endDate time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*TimeSeries, error) { - if startDate.IsZero() || endDate.IsZero() { - return nil, errors.New("startDate and endDate must be set") - } - - if startDate.After(endDate) || startDate.Equal(endDate) { - return nil, errors.New("startDate and endDate must be set correctly") - } - - v := url.Values{} - v.Set("start_date", startDate.UTC().Format(timeLayout)) - v.Set("end_date", endDate.UTC().Format(timeLayout)) - - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var t TimeSeries - return &t, e.SendHTTPRequest("timeseries", v, &t) -} - -// GetFluctuations returns a list of forex price fluctuations based on the supplied params -func (e *ExchangeRateHost) GetFluctuations(startDate, endDate time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*Fluctuations, error) { - if startDate.IsZero() || endDate.IsZero() { - return nil, errors.New("startDate and endDate must be set") - } - - if startDate.After(endDate) || startDate.Equal(endDate) { - return nil, errors.New("startDate and endDate must be set correctly") - } - - v := url.Values{} - v.Set("start_date", startDate.UTC().Format(timeLayout)) - v.Set("end_date", endDate.UTC().Format(timeLayout)) - - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var f Fluctuations - return &f, e.SendHTTPRequest("fluctuation", v, &f) -} - -// GetSupportedSymbols returns a list of supported symbols -func (e *ExchangeRateHost) GetSupportedSymbols() (*SupportedSymbols, error) { - var s SupportedSymbols - return &s, e.SendHTTPRequest("symbols", url.Values{}, &s) -} - -// GetSupportedCurrencies returns a list of supported currencies -func (e *ExchangeRateHost) GetSupportedCurrencies() ([]string, error) { - s, err := e.GetSupportedSymbols() - if err != nil { - return nil, err - } - - symbols := make([]string, 0, len(s.Symbols)) - for x := range s.Symbols { - symbols = append(symbols, x) - } - return symbols, nil -} - -// GetRates returns the forex rates based on the supplied base currency and symbols -func (e *ExchangeRateHost) GetRates(baseCurrency, symbols string) (map[string]float64, error) { - l, err := e.GetLatestRates(baseCurrency, symbols, 0, 0, "") - if err != nil { - return nil, err - } - - rates := make(map[string]float64) - for k, v := range l.Rates { - rates[baseCurrency+k] = v - } - return rates, nil -} - -// SendHTTPRequest sends a typical get request -func (e *ExchangeRateHost) SendHTTPRequest(endpoint string, v url.Values, result interface{}) error { - path := common.EncodeURLValues(exchangeRateHostURL+"/"+endpoint, v) - item := &request.Item{ - Method: http.MethodGet, - Path: path, - Result: &result, - Verbose: e.Verbose, - } - return e.Requester.SendPayload(context.TODO(), request.Unset, func() (*request.Item, error) { - return item, nil - }, request.UnauthenticatedRequest) -} diff --git a/currency/forexprovider/exchangerate.host/exchangerate_test.go b/currency/forexprovider/exchangerate.host/exchangerate_test.go deleted file mode 100644 index c5535909..00000000 --- a/currency/forexprovider/exchangerate.host/exchangerate_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package exchangeratehost - -import ( - "log" - "os" - "testing" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -) - -var ( - e ExchangeRateHost - testCurrencies = "USD,EUR,CZK" -) - -func TestMain(t *testing.M) { - err := e.Setup(base.Settings{ - Name: "ExchangeRateHost", - }) - if err != nil { - log.Fatal(err) - } - os.Exit(t.Run()) -} - -func TestGetLatestRates(t *testing.T) { - _, err := e.GetLatestRates("USD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestConvertCurrency(t *testing.T) { - _, err := e.ConvertCurrency("USD", "EUR", "", testCurrencies, "", time.Now(), 1200, 2) - if err != nil { - t.Error(err) - } -} - -func TestGetHistoricRates(t *testing.T) { - _, err := e.GetHistoricalRates(time.Time{}, "AUD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestGetTimeSeriesRates(t *testing.T) { - _, err := e.GetTimeSeries(time.Time{}, time.Now(), "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("empty start time show throw an error") - } - tmNow := time.Now() - _, err = e.GetTimeSeries(tmNow, tmNow, "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("equal times show throw an error") - } - tmStart := tmNow.AddDate(0, -3, 0) - _, err = e.GetTimeSeries(tmStart, tmNow, "USD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestGetFluctuationData(t *testing.T) { - _, err := e.GetFluctuations(time.Time{}, time.Now(), "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("empty start time show throw an error") - } - tmNow := time.Now() - _, err = e.GetFluctuations(tmNow, tmNow, "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("equal times show throw an error") - } - tmStart := tmNow.AddDate(0, -3, 0) - _, err = e.GetFluctuations(tmStart, tmNow, "USD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestGetSupportedSymbols(t *testing.T) { - r, err := e.GetSupportedSymbols() - if err != nil { - t.Fatal(err) - } - if _, ok := r.Symbols["AUD"]; !ok { - t.Error("should contain AUD") - } -} - -func TestGetGetSupportedCurrencies(t *testing.T) { - s, err := e.GetSupportedCurrencies() - if err != nil { - t.Fatal(err) - } - if len(s) == 0 { - t.Error("supported currencies should be greater than 0") - } -} - -func TestGetRates(t *testing.T) { - r, err := e.GetRates("USD", "") - if err != nil { - t.Fatal(err) - } - if rate := r["USDAUD"]; rate == 0 { - t.Error("rate of USDAUD should be set") - } -} diff --git a/currency/forexprovider/exchangerate.host/exchangerate_types.go b/currency/forexprovider/exchangerate.host/exchangerate_types.go deleted file mode 100644 index 0a56ac98..00000000 --- a/currency/forexprovider/exchangerate.host/exchangerate_types.go +++ /dev/null @@ -1,91 +0,0 @@ -package exchangeratehost - -import ( - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" -) - -// ExchangeRateHost stores the struct for the exchangerate.host API -type ExchangeRateHost struct { - base.Base - Requester *request.Requester -} - -// MessageOfTheDay stores the message of the day -type MessageOfTheDay struct { - Message string `json:"msg"` - DonationURL string `json:"url"` -} - -// LatestRates stores the latest forex rates -type LatestRates struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - Base string `json:"base"` - Date string `json:"date"` - Rates map[string]float64 `json:"rates"` -} - -// ConvertCurrency stores currency conversion data -type ConvertCurrency struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Query struct { - From string `json:"from"` - To string `json:"to"` - Amount float64 `json:"amount"` - } `json:"query"` - Info struct { - Rate float64 `json:"rate"` - } `json:"info"` - Historical bool `json:"historical"` - Date string `json:"date"` - Result float64 `json:"result"` -} - -// HistoricRates stores the hostoric rates -type HistoricRates struct { - LatestRates - Historical bool `json:"historical"` -} - -// TimeSeries stores time series data -type TimeSeries struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - TimeSeries bool `json:"timeseries"` - Base string `json:"base"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - Rates map[string]map[string]float64 `json:"rates"` -} - -// Fluctuation stores an individual rate flucutation -type Fluctuation struct { - StartRate float64 `json:"start_rate"` - EndRate float64 `json:"end_rate"` - Change float64 `json:"change"` - ChangePercentage float64 `json:"change_pct"` -} - -// Fluctuations stores a collection of rate fluctuations -type Fluctuations struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - Flucutation bool `json:"fluctuation"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - Rates map[string]Fluctuation `json:"rate"` -} - -// Symbol stores an individual symbol -type Symbol struct { - Description string `json:"description"` - Code string `json:"code"` -} - -// SupportedSymbols store a collection of supported symbols -type SupportedSymbols struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - Symbols map[string]Symbol `json:"symbols"` -} diff --git a/currency/forexprovider/forexprovider.go b/currency/forexprovider/forexprovider.go index 393a01fa..0be87517 100644 --- a/currency/forexprovider/forexprovider.go +++ b/currency/forexprovider/forexprovider.go @@ -9,7 +9,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" currencyconverter "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverterapi" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer" - exchangeratehost "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host" exchangerates "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangeratesapi.io" fixer "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" @@ -33,36 +32,9 @@ func GetSupportedForexProviders() []string { "ExchangeRates", "Fixer", "OpenExchangeRates", - "ExchangeRateHost", } } -// NewDefaultFXProvider returns the default forex provider (currencyconverterAPI) -func NewDefaultFXProvider() *ForexProviders { - handler := new(ForexProviders) - provider := new(exchangeratehost.ExchangeRateHost) - err := provider.Setup(base.Settings{ - PrimaryProvider: true, - Enabled: true, - Name: "ExchangeRateHost", - }) - if err != nil { - panic(err) - } - - currencies, _ := provider.GetSupportedCurrencies() - providerBase := base.Provider{ - Provider: provider, - SupportedCurrencies: currencies, - } - - handler.FXHandler = base.FXHandler{ - Primary: providerBase, - } - - return handler -} - // SetProvider sets provider to the FX handler func (f *ForexProviders) SetProvider(b base.IFXProvider) error { currencies, err := b.GetSupportedCurrencies() @@ -102,8 +74,6 @@ func StartFXService(fxProviders []base.Settings) (*ForexProviders, error) { provider = new(fixer.Fixer) case "OpenExchangeRates": provider = new(openexchangerates.OXR) - case "ExchangeRateHost": - provider = new(exchangeratehost.ExchangeRateHost) default: return nil, fmt.Errorf("%s %w", fxProviders[i].Name, errUnhandledForeignExchangeProvider) diff --git a/currency/mock_provider_test.go b/currency/mock_provider_test.go new file mode 100644 index 00000000..3e1431ef --- /dev/null +++ b/currency/mock_provider_test.go @@ -0,0 +1,40 @@ +package currency + +import ( + "math/rand" + "strings" + + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" +) + +type MockProvider struct{} + +func newMockProvider() *forexprovider.ForexProviders { + p := &MockProvider{} + c, _ := p.GetSupportedCurrencies() + return &forexprovider.ForexProviders{ + FXHandler: base.FXHandler{ + Primary: base.Provider{ + Provider: p, + SupportedCurrencies: c, + }, + }, + } +} + +func (m *MockProvider) GetName() string { return "MockProvider" } +func (m *MockProvider) Setup(base.Settings) error { return nil } +func (m *MockProvider) IsEnabled() bool { return true } +func (m *MockProvider) IsPrimaryProvider() bool { return true } +func (m *MockProvider) GetSupportedCurrencies() ([]string, error) { + return storage.defaultFiatCurrencies.Strings(), nil +} +func (m *MockProvider) GetRates(baseCurrency, symbols string) (map[string]float64, error) { + c := map[string]float64{} + for _, s := range strings.Split(symbols, ",") { + // The year is 2027; The USD is nearly worthless. The world reserve currency is eggs. + c[baseCurrency+s] = 1 / (1 + rand.Float64()) //nolint:gosec // Doesn't need to be a strong random number + } + return c, nil +} diff --git a/currency/storage.go b/currency/storage.go index e43d7b35..930533f1 100644 --- a/currency/storage.go +++ b/currency/storage.go @@ -26,10 +26,9 @@ func init() { // CurrencyFileUpdateDelay defines the rate at which the currency.json file is // updated const ( - DefaultCurrencyFileDelay = 168 * time.Hour - DefaultForeignExchangeDelay = 1 * time.Minute - DefaultStorageFile = "currency.json" - DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost" + DefaultCurrencyFileDelay = 168 * time.Hour + DefaultForeignExchangeDelay = 1 * time.Minute + DefaultStorageFile = "currency.json" ) var ( @@ -74,7 +73,12 @@ func (s *Storage) SetDefaults() { log.Errorf(log.Currency, "Currency Storage: Setting default cryptocurrencies error: %s", err) } s.SetupConversionRates() - s.fiatExchangeMarkets = forexprovider.NewDefaultFXProvider() + s.fiatExchangeMarkets = nil +} + +// ForexEnabled returns whether the currency system has any available forex providers enabled +func ForexEnabled() bool { + return storage.fiatExchangeMarkets != nil } // RunUpdater runs the foreign exchange updater service. This will set up a JSON @@ -103,6 +107,10 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath } s.mtx.Lock() + + // Ensure the forex provider is unset in cases we exit early + s.fiatExchangeMarkets = nil + s.shutdown = make(chan struct{}) s.baseCurrency = settings.FiatDisplayCurrency s.path = filepath.Join(filePath, DefaultStorageFile) @@ -132,26 +140,23 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath (settings.ForexProviders[i].Name == "CurrencyLayer" && overrides.CurrencyLayer) || (settings.ForexProviders[i].Name == "Fixer" && overrides.Fixer) || (settings.ForexProviders[i].Name == "OpenExchangeRates" && overrides.OpenExchangeRates) || - (settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) || - (settings.ForexProviders[i].Name == "ExchangeRateHost" && overrides.ExchangeRateHost) + (settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) if !enabled { continue } - if settings.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI { - if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" { - log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n", - settings.ForexProviders[i].Name) - settings.ForexProviders[i].Enabled = false - settings.ForexProviders[i].PrimaryProvider = false - continue - } + if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" { + log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n", + settings.ForexProviders[i].Name) + settings.ForexProviders[i].Enabled = false + settings.ForexProviders[i].PrimaryProvider = false + continue + } - if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" { - log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n", - settings.ForexProviders[i].Name) - } + if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" { + log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n", + settings.ForexProviders[i].Name) } if settings.ForexProviders[i].PrimaryProvider { @@ -166,23 +171,10 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[i])) } - if len(fxSettings) == 0 { - log.Warnln(log.Currency, "No foreign exchange providers enabled, setting default provider...") - for x := range settings.ForexProviders { - if settings.ForexProviders[x].Name != DefaultForexProviderExchangeRatesAPI { - continue - } - settings.ForexProviders[x].Enabled = true - settings.ForexProviders[x].PrimaryProvider = true - primaryProvider = true - log.Warnf(log.Currency, "No valid foreign exchange providers configured. Defaulting to %s.", DefaultForexProviderExchangeRatesAPI) - fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[x])) - } - } - if len(fxSettings) == 0 { s.mtx.Unlock() - return errNoForeignExchangeProvidersEnabled + log.Warnln(log.Currency, "No foreign exchange providers enabled, currency conversion will not be available") + return nil } if !primaryProvider { @@ -313,8 +305,7 @@ func (s *Storage) ForeignExchangeUpdater() { log.Errorln(log.Currency, err) } - // Unlock main rate retrieval mutex so all routines waiting can get access - // to data + // Unlock main rate retrieval mutex so all routines waiting can get access to data s.mtx.Unlock() // Set tickers to client defined rates or defaults @@ -516,6 +507,9 @@ func (s *Storage) UpdateCurrencies() error { func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error { s.fxRates.mtx.Lock() defer s.fxRates.mtx.Unlock() + if s.fiatExchangeMarkets == nil { + return nil + } rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(), c.Strings()) if err != nil { @@ -526,6 +520,9 @@ func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error { // SeedForeignExchangeRate returns a singular exchange rate func (s *Storage) SeedForeignExchangeRate(from, to Code) (map[string]float64, error) { + if s.fiatExchangeMarkets == nil { + return nil, nil + } return s.fiatExchangeMarkets.GetCurrencyData(from.String(), []string{to.String()}) } @@ -546,6 +543,9 @@ func (s *Storage) GetDefaultForeignExchangeRates() (Conversions, error) { func (s *Storage) SeedDefaultForeignExchangeRates() error { s.fxRates.mtx.Lock() defer s.fxRates.mtx.Unlock() + if s.fiatExchangeMarkets == nil { + return errNoForeignExchangeProvidersEnabled + } rates, err := s.fiatExchangeMarkets.GetCurrencyData( s.defaultBaseCurrency.String(), s.defaultFiatCurrencies.Strings()) @@ -566,11 +566,13 @@ func (s *Storage) GetExchangeRates() (Conversions, error) { return s.fxRates.GetFullRates(), nil } -// SeedForeignExchangeRates seeds the foreign exchange rates from storage config -// currencies +// SeedForeignExchangeRates seeds the foreign exchange rates from storage config currencies func (s *Storage) SeedForeignExchangeRates() error { s.fxRates.mtx.Lock() defer s.fxRates.mtx.Unlock() + if s.fiatExchangeMarkets == nil { + return errNoForeignExchangeProvidersEnabled + } rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(), s.fiatCurrencies.Strings()) if err != nil { @@ -695,6 +697,9 @@ func (s *Storage) UpdateEnabledFiatCurrencies(c Currencies) { // ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen // or vice versa. func (s *Storage) ConvertCurrency(amount float64, from, to Code) (float64, error) { + if s.fiatExchangeMarkets == nil { + return 0, errNoForeignExchangeProvidersEnabled + } if amount <= 0 { return 0, fmt.Errorf("%f %w", amount, errInvalidAmount) } diff --git a/currency/storage_test.go b/currency/storage_test.go index 8481ac4a..a35b60dd 100644 --- a/currency/storage_test.go +++ b/currency/storage_test.go @@ -1,11 +1,11 @@ package currency import ( - "errors" "fmt" "os" "testing" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/database/testhelpers" ) @@ -17,6 +17,8 @@ func TestMain(m *testing.M) { os.Exit(1) } + storage.fiatExchangeMarkets = newMockProvider() + t := m.Run() err = os.RemoveAll(testhelpers.TempDir) @@ -30,219 +32,88 @@ func TestMain(m *testing.M) { func TestRunUpdater(t *testing.T) { var newStorage Storage - emptyMainConfig := Config{} - err := newStorage.RunUpdater(BotOverrides{}, &emptyMainConfig, "") - if err == nil { - t.Fatal("storage RunUpdater() error cannot be nil") - } + err := newStorage.RunUpdater(BotOverrides{}, &Config{}, "") + assert.ErrorIs(t, err, errFiatDisplayCurrencyUnset, "No currency should error correctly") - mainConfig := Config{} - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "") - if !errors.Is(err, errFiatDisplayCurrencyUnset) { - t.Fatalf("received: '%v' but expected: '%v'", err, errFiatDisplayCurrencyUnset) - } + err = newStorage.RunUpdater(BotOverrides{}, &Config{FiatDisplayCurrency: BTC}, "") + assert.ErrorIs(t, err, ErrFiatDisplayCurrencyIsNotFiat, "Crypto currency should error as not fiat") - mainConfig.FiatDisplayCurrency = BTC - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "") - if !errors.Is(err, ErrFiatDisplayCurrencyIsNotFiat) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrFiatDisplayCurrencyIsNotFiat) - } - - mainConfig.FiatDisplayCurrency = AUD - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "") - if !errors.Is(err, errNoFilePathSet) { - t.Fatalf("received: '%v' but expected: '%v'", err, errNoFilePathSet) - } + c := &Config{FiatDisplayCurrency: AUD} + err = newStorage.RunUpdater(BotOverrides{}, c, "") + assert.ErrorIs(t, err, errNoFilePathSet, "Should error with no path set") tempDir := testhelpers.TempDir + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) + assert.ErrorIs(t, err, errInvalidCurrencyFileUpdateDuration, "Should error invalid file update duration") - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir) - if !errors.Is(err, errInvalidCurrencyFileUpdateDuration) { - t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCurrencyFileUpdateDuration) - } + c.CurrencyFileUpdateDuration = DefaultCurrencyFileDelay + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) + assert.ErrorIs(t, err, errInvalidForeignExchangeUpdateDuration, "Should error invalid forex update duration") - mainConfig.CurrencyFileUpdateDuration = DefaultCurrencyFileDelay - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir) - if !errors.Is(err, errInvalidForeignExchangeUpdateDuration) { - t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidForeignExchangeUpdateDuration) - } + c.ForeignExchangeUpdateDuration = DefaultForeignExchangeDelay + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) - mainConfig.ForeignExchangeUpdateDuration = DefaultForeignExchangeDelay - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir) - if !errors.Is(err, errNoForeignExchangeProvidersEnabled) { - t.Fatalf("received: '%v' but expected: '%v'", err, errNoForeignExchangeProvidersEnabled) - } - - settings := FXSettings{ - Name: "Fixer", - Enabled: true, - APIKey: "wo", - } - - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{Fixer: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } + assert.NoError(t, err, "Storage should not error with no forex providers enabled") + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") // Proxy for testing ForexEnabled err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) + assert.NoError(t, err, "Shutdown should not error evne though it silently aborted the RunUpdater early") + + // Exchanges which reject a bad APIKey + for _, n := range []string{"Fixer", "CurrencyConverter", "CurrencyLayer", "ExchangeRates"} { + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: ""}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.NoErrorf(t, err, "%s should not error and silently exit without running with no api keys", n) + assert.Falsef(t, c.ForexProviders[0].Enabled, "%s should not be marked enabled with no api keys", n) + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: "sudo shazam!"}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.Errorf(t, err, "%s should throw some provider originating error with a (hopefully) invalid api key", n) + assert.Truef(t, c.ForexProviders[0].Enabled, "%s should still be enabled after being chosen but failing", n) + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled when provider errored during startup") + err = newStorage.Shutdown() + assert.NoError(t, err, "Shutdown should not error") } - settings.Name = "CurrencyConverter" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{CurrencyConverter: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") + // Exchanges which do not error with a bad APIKey on startup + for _, n := range []string{"OpenExchangeRates"} { + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: ""}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.NoErrorf(t, err, "%s should not error and silently exit without running with no api keys", n) + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: "sudo shazam!"}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.NoError(t, err, "%s should not error on Setup with a bad apikey", n) + assert.NotNil(t, newStorage.fiatExchangeMarkets, "Forex should be enabled now we have a provider with a key") + err = newStorage.Shutdown() + assert.NoError(t, err, "Shutdown should not error") } - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) + c.ForexProviders = AllFXSettings{ + {Name: "ExchangeRates"}, // Old Default + {Name: "OpenExchangeRates", APIKey: "shazam?"}, } - settings.Name = "CurrencyLayer" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{CurrencyLayer: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "OpenExchangeRates" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{OpenExchangeRates: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "ExchangeRates" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "ExchangeRateHost" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{ExchangeRateHost: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - // old config where two providers enabled - oldDefault := FXSettings{ - Name: "ExchangeRates", - Enabled: true, - APIKey: "", // old default provider which did not need api keys. - PrimaryProvider: true, - } - other := FXSettings{ - Name: "OpenExchangeRates", - Enabled: true, - APIKey: "enabled-keys", // Has keys enabled and will fall over to primary - } - defaultProvider := FXSettings{ - // From config this will be included but not necessarily enabled. - Name: "ExchangeRateHost", - Enabled: false, - } - - mainConfig.ForexProviders = AllFXSettings{oldDefault, other, defaultProvider} - err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true, OpenExchangeRates: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if mainConfig.ForexProviders[0].Enabled { - t.Fatal("old default provider should not be enabled due to unset keys") - } - - if mainConfig.ForexProviders[0].PrimaryProvider { - t.Fatal("old default provider should not be a primary provider anymore") - } - - if !mainConfig.ForexProviders[1].Enabled { - t.Fatal("open exchange rates provider with keys set should be enabled") - } - - if !mainConfig.ForexProviders[1].PrimaryProvider { - t.Fatal("open exchange rates provider with keys set should be set as primary provider") - } - - if mainConfig.ForexProviders[2].Enabled { - t.Fatal("actual default provider should not be enabled") - } - - if mainConfig.ForexProviders[2].PrimaryProvider { - t.Fatal("actual default provider should not be designated as primary provider") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - // old config where two providers enabled - oldDefault = FXSettings{ - Name: "ExchangeRates", - Enabled: true, - APIKey: "", // old default provider which did not need api keys. - PrimaryProvider: true, - } - other = FXSettings{ - Name: "OpenExchangeRates", - Enabled: true, - APIKey: "", // Has no keys enabled will fall over to new default provider. - } - - mainConfig.ForexProviders = AllFXSettings{oldDefault, other, defaultProvider} - err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true, OpenExchangeRates: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if mainConfig.ForexProviders[0].Enabled { - t.Fatal("old default provider should not be enabled due to unset keys") - } - - if mainConfig.ForexProviders[0].PrimaryProvider { - t.Fatal("old default provider should not be a primary provider anymore") - } - - if mainConfig.ForexProviders[1].Enabled { - t.Fatal("open exchange rates provider with keys unset should not be enabled") - } - - if mainConfig.ForexProviders[1].PrimaryProvider { - t.Fatal("open exchange rates provider with keys unset should not be set as primary provider") - } - - if !mainConfig.ForexProviders[2].Enabled { - t.Fatal("actual default provider should not be disabled") - } - - if !mainConfig.ForexProviders[2].PrimaryProvider { - t.Fatal("actual default provider should be designated as primary provider") - } + // Regression test for old defaults which were enabled when in settings and nothing else was enabled and configured + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) + assert.NoError(t, err, "RunUpdater should not error") + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") // Proxy for testing ForexEnabled + assert.False(t, c.ForexProviders[0].Enabled, "Old Default ExchangeRates should not have defaulted to enabled with no enabled overrides") +} + +func overrideForProvider(n string) BotOverrides { + b := BotOverrides{} + switch n { + case "Fixer": + b.Fixer = true + case "CurrencyConverter": + b.CurrencyConverter = true + case "CurrencyLayer": + b.CurrencyLayer = true + case "OpenExchangeRates": + b.OpenExchangeRates = true + case "ExchangeRates": + b.ExchangeRates = true + } + return b } diff --git a/engine/engine.go b/engine/engine.go index d279afa8..08bb6041 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -173,7 +173,6 @@ func validateSettings(b *Engine, s *Settings, flagSet FlagSet) { flagSet.WithBool("exchangerates", &b.Settings.EnableExchangeRates, b.Config.Currency.ForexProviders.IsEnabled("exchangerates")) flagSet.WithBool("fixer", &b.Settings.EnableFixer, b.Config.Currency.ForexProviders.IsEnabled("fixer")) flagSet.WithBool("openexchangerates", &b.Settings.EnableOpenExchangeRates, b.Config.Currency.ForexProviders.IsEnabled("openexchangerates")) - flagSet.WithBool("exchangeratehost", &b.Settings.EnableExchangeRateHost, b.Config.Currency.ForexProviders.IsEnabled("exchangeratehost")) flagSet.WithBool("datahistorymanager", &b.Settings.EnableDataHistoryManager, b.Config.DataHistoryManager.Enabled) flagSet.WithBool("currencystatemanager", &b.Settings.EnableCurrencyStateManager, b.Config.CurrencyStateManager.Enabled != nil && *b.Config.CurrencyStateManager.Enabled) @@ -403,12 +402,11 @@ func (bot *Engine) Start() error { ExchangeRates: bot.Settings.EnableExchangeRates, Fixer: bot.Settings.EnableFixer, OpenExchangeRates: bot.Settings.EnableOpenExchangeRates, - ExchangeRateHost: bot.Settings.EnableExchangeRateHost, }, &bot.Config.Currency, bot.Settings.DataDir, ); err != nil { - gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %s", err) + gctlog.Errorf(gctlog.Global, "Currency Converter system failed to start %s", err) } if bot.Settings.EnableGRPC { @@ -677,7 +675,7 @@ func (bot *Engine) Stop() { err = currency.ShutdownStorageUpdater() if err != nil { - gctlog.Errorf(gctlog.Global, "ExchangeSettings storage system. Error: %v", err) + gctlog.Errorf(gctlog.Global, "Currency Converter unable to stop. Error: %v", err) } if !bot.Settings.EnableDryRun { diff --git a/engine/engine_types.go b/engine/engine_types.go index 211c7140..c84b940e 100644 --- a/engine/engine_types.go +++ b/engine/engine_types.go @@ -80,7 +80,6 @@ type ForexSettings struct { EnableExchangeRates bool EnableFixer bool EnableOpenExchangeRates bool - EnableExchangeRateHost bool } // ExchangeTuningSettings defines settings related to an exchange diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 52dd075e..22db7095 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -697,13 +697,15 @@ func printCurrencyFormat(price float64, displayCurrency currency.Code) string { } func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency currency.Code) string { - var conv float64 + var conv string if origPrice > 0 { - var err error - conv, err = currency.ConvertFiat(origPrice, origCurrency, displayCurrency) - if err != nil { - log.Errorf(log.SyncMgr, "Failed to convert currency: %s", err) + if convFloat, err := currency.ConvertFiat(origPrice, origCurrency, displayCurrency); err != nil { + conv = "?.??" + } else { + conv = fmt.Sprintf("%.2f", convFloat) } + } else { + conv = "0.00" } displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) @@ -718,7 +720,7 @@ func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency err) } - return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", + return fmt.Sprintf("%s%s %s (%s%.2f %s)", displaySymbol, conv, displayCurrency, @@ -755,7 +757,8 @@ func (m *SyncManager) PrintTickerSummary(result *ticker.Price, protocol string, return } - if result.Pair.Quote.IsFiatCurrency() && + if currency.ForexEnabled() && + result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty() { origCurrency := result.Pair.Quote.Upper() @@ -854,7 +857,7 @@ func (m *SyncManager) PrintOrderbookSummary(result *orderbook.Base, protocol str var bidValueResult, askValueResult string switch { - case result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty(): + case currency.ForexEnabled() && result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty(): origCurrency := result.Pair.Quote.Upper() if bidsValue > 0 { bidValueResult = printConvertCurrencyFormat(bidsValue, origCurrency, m.fiatDisplayCurrency) diff --git a/main.go b/main.go index fd0900ee..a6085d4f 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,6 @@ func main() { flag.BoolVar(&settings.EnableExchangeRates, "exchangerates", false, "overrides config and sets up foreign exchange exchangeratesapi.io") flag.BoolVar(&settings.EnableFixer, "fixer", false, "overrides config and sets up foreign exchange Fixer.io") flag.BoolVar(&settings.EnableOpenExchangeRates, "openexchangerates", false, "overrides config and sets up foreign exchange Open Exchange Rates") - flag.BoolVar(&settings.EnableExchangeRateHost, "exchangeratehost", false, "overrides config and sets up foreign exchange ExchangeRate.host") // Exchange tuning settings flag.BoolVar(&settings.EnableExchangeAutoPairUpdates, "exchangeautopairupdates", false, "enables automatic available currency pair updates for supported exchanges")