Files
gocryptotrader/config/config.go
Scott c2a33300f5 Feature+Bugfix: Engine websocket management (#360)
* Initial commit tearing down the websocket connection management. The purpose is to remove the traffic monitoring and dropping as syncer.go is a better manager

* Adds a readwrite mutex and helper functions to minimise inline lock/unlocks and prevent races

* Creates new WebsocketType struct to contain all parameters required. Deletes WebsocketReset. Utilises ReadMessageErrors channel for all websocket readmessages to analyse when an error returned is due to a disconnect

* Fixes issue with syncer trying to connect while connecting

* Simplifies initialisation function for websocket. Reconnects and resubscribes after disconnection

* Adds WebsocketTimeout config value to dictate when the websocket traffic monitor should die. Default to two minutes of no traffic activity. Increases test coverage and updates existing tests to work with new technologic. RE-ADDS TESTS I ACCIDENTALLY DELETED FROM PREVIOUS PR

* Removes snapshot override as its always necessary when considering reconnections. Increases test coverage. Re-adds tests that were ACCIDENTALLY DELETED. Removes unused websocket channels. Bug fix for traffic monitor to shutdown via goroutine instead of killing itself

* Fixes gateio bug for authentication errors when null. Adds little entry to syncer for when websocket is switched to rest and then back, you get a log notifying of the return. Fixes okgroup bug where ws message is sent on a disconnected ws, causing panic. Renames setConnectionStatus to setConnectedStatus. Puts connection monitor log behind verbose bool

* Fixes lingering races. Fixes bug where websocket was enabled whether you liked it or not. Removes demonstration test

* Fixes log message, renames unc, removes comments

* Fixes data race

* Removes verbosity, ensures shutdown sets connection status appropriately

* Removes go routine causing CPU spike. Stops timers properly and resets timers properly

* Renames `WsEnabled` to `Enabled`. Increases test coverage. Fixes typos. Handles unhandled errors

* The forgotten lint

* With using RWlocks, removes the channel nil check and relies on !w.IsConnected() to prevent a shutdown from recurring

* Removes extra closure step in the defer as it causes all the issues

* Prevents timer channel hangups. Minimises use of websocket Connect(). Expands disconnection error definition. Removes routine disconnection error handling. Ensures only one traffic monitor can ever be run. Renames subscriptionLock to subscriptionMutext for consistency

* Extends timeout to 30 seconds to cover for non-popular exchanges and non-popular currencies

* Updates test from rebase to use new websocket setup function

* Fixes test to ensure it tests what it says it does
2019-10-02 09:06:52 +10:00

1730 lines
51 KiB
Go

package config
import (
"bufio"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/connchecker"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
log "github.com/thrasher-corp/gocryptotrader/logger"
)
// Constants declared here are filename strings and test strings
const (
FXProviderFixer = "fixer"
EncryptedConfigFile = "config.dat"
ConfigFile = "config.json"
ConfigTestFile = "../testdata/configtest.json"
configFileEncryptionPrompt = 0
configFileEncryptionEnabled = 1
configFileEncryptionDisabled = -1
configPairsLastUpdatedWarningThreshold = 30 // 30 days
configDefaultHTTPTimeout = time.Second * 15
configDefaultWebsocketResponseCheckTimeout = time.Millisecond * 30
configDefaultWebsocketResponseMaxLimit = time.Second * 7
configDefaultWebsocketOrderbookBufferLimit = 5
configDefaultWebsocketTrafficTimeout = time.Second * 30
configMaxAuthFailures = 3
defaultNTPAllowedDifference = 50000000
defaultNTPAllowedNegativeDifference = 50000000
DefaultAPIKey = "Key"
DefaultAPISecret = "Secret"
DefaultAPIClientID = "ClientID"
)
// 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!"
)
// 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 = "ExchangeRates"
)
// Variables here are used for configuration
var (
Cfg Config
IsInitialSetup bool
testBypass bool
m sync.Mutex
)
// GetCurrencyConfig returns currency configurations
func (c *Config) GetCurrencyConfig() CurrencyConfig {
return c.Currency
}
// GetExchangeBankAccounts returns banking details associated with an exchange
// for depositing funds
func (c *Config) GetExchangeBankAccounts(exchangeName, depositingCurrency string) (BankAccount, error) {
m.Lock()
defer m.Unlock()
for x := range c.Exchanges {
if strings.EqualFold(c.Exchanges[x].Name, exchangeName) {
for y := range c.Exchanges[x].BankAccounts {
if strings.Contains(c.Exchanges[x].BankAccounts[y].SupportedCurrencies,
depositingCurrency) {
return c.Exchanges[x].BankAccounts[y], nil
}
}
}
}
return BankAccount{}, fmt.Errorf("exchange %s bank details not found for %s",
exchangeName,
depositingCurrency)
}
// UpdateExchangeBankAccounts updates the configuration for the associated
// exchange bank
func (c *Config) UpdateExchangeBankAccounts(exchangeName string, bankCfg []BankAccount) error {
m.Lock()
defer m.Unlock()
for i := range c.Exchanges {
if strings.EqualFold(c.Exchanges[i].Name, exchangeName) {
c.Exchanges[i].BankAccounts = bankCfg
return nil
}
}
return fmt.Errorf("exchange %s not found",
exchangeName)
}
// GetClientBankAccounts returns banking details used for a given exchange
// and currency
func (c *Config) GetClientBankAccounts(exchangeName, targetCurrency string) (BankAccount, error) {
m.Lock()
defer m.Unlock()
for x := range c.BankAccounts {
if (strings.Contains(c.BankAccounts[x].SupportedExchanges, exchangeName) ||
c.BankAccounts[x].SupportedExchanges == "ALL") &&
strings.Contains(c.BankAccounts[x].SupportedCurrencies, targetCurrency) {
return c.BankAccounts[x], nil
}
}
return BankAccount{}, fmt.Errorf("client banking details not found for %s and currency %s",
exchangeName,
targetCurrency)
}
// UpdateClientBankAccounts updates the configuration for a bank
func (c *Config) UpdateClientBankAccounts(bankCfg *BankAccount) error {
m.Lock()
defer m.Unlock()
for i := range c.BankAccounts {
if c.BankAccounts[i].BankName == bankCfg.BankName && c.BankAccounts[i].AccountNumber == bankCfg.AccountNumber {
c.BankAccounts[i] = *bankCfg
return nil
}
}
return fmt.Errorf("client banking details for %s not found, update not applied",
bankCfg.BankName)
}
// CheckClientBankAccounts checks client bank details
func (c *Config) CheckClientBankAccounts() {
m.Lock()
defer m.Unlock()
if len(c.BankAccounts) == 0 {
c.BankAccounts = append(c.BankAccounts,
BankAccount{
BankName: "Test Bank",
BankAddress: "42 Bank Street",
BankPostalCode: "13337",
BankPostalCity: "Satoshiville",
BankCountry: "Japan",
AccountName: "Satoshi Nakamoto",
AccountNumber: "0234",
SWIFTCode: "91272837",
IBAN: "98218738671897",
SupportedCurrencies: "USD",
SupportedExchanges: "ANX,Kraken",
},
)
return
}
for i := range c.BankAccounts {
if c.BankAccounts[i].Enabled {
err := c.BankAccounts[i].Validate()
if err != nil {
c.BankAccounts[i].Enabled = false
log.Warn(log.ConfigMgr, err.Error())
}
}
}
}
// PurgeExchangeAPICredentials purges the stored API credentials
func (c *Config) PurgeExchangeAPICredentials() {
m.Lock()
defer m.Unlock()
for x := range c.Exchanges {
if !c.Exchanges[x].API.AuthenticatedSupport && !c.Exchanges[x].API.AuthenticatedWebsocketSupport {
continue
}
c.Exchanges[x].API.AuthenticatedSupport = false
c.Exchanges[x].API.AuthenticatedWebsocketSupport = false
if c.Exchanges[x].API.CredentialsValidator.RequiresKey {
c.Exchanges[x].API.Credentials.Key = DefaultAPIKey
}
if c.Exchanges[x].API.CredentialsValidator.RequiresSecret {
c.Exchanges[x].API.Credentials.Secret = DefaultAPISecret
}
if c.Exchanges[x].API.CredentialsValidator.RequiresClientID {
c.Exchanges[x].API.Credentials.ClientID = DefaultAPIClientID
}
c.Exchanges[x].API.Credentials.PEMKey = ""
c.Exchanges[x].API.Credentials.OTPSecret = ""
}
}
// GetCommunicationsConfig returns the communications configuration
func (c *Config) GetCommunicationsConfig() CommunicationsConfig {
m.Lock()
defer m.Unlock()
return c.Communications
}
// UpdateCommunicationsConfig sets a new updated version of a Communications
// configuration
func (c *Config) UpdateCommunicationsConfig(config *CommunicationsConfig) {
m.Lock()
c.Communications = *config
m.Unlock()
}
// GetCryptocurrencyProviderConfig returns the communications configuration
func (c *Config) GetCryptocurrencyProviderConfig() CryptocurrencyProvider {
m.Lock()
defer m.Unlock()
return c.Currency.CryptocurrencyProvider
}
// UpdateCryptocurrencyProviderConfig returns the communications configuration
func (c *Config) UpdateCryptocurrencyProviderConfig(config CryptocurrencyProvider) {
m.Lock()
c.Currency.CryptocurrencyProvider = config
m.Unlock()
}
// CheckCommunicationsConfig checks to see if the variables are set correctly
// from config.json
func (c *Config) CheckCommunicationsConfig() {
m.Lock()
defer m.Unlock()
// If the communications config hasn't been populated, populate
// with example settings
if c.Communications.SlackConfig.Name == "" {
c.Communications.SlackConfig = SlackConfig{
Name: "Slack",
TargetChannel: "general",
VerificationToken: "testtest",
}
}
if c.Communications.SMSGlobalConfig.Name == "" {
if c.SMS != nil {
if c.SMS.Contacts != nil {
c.Communications.SMSGlobalConfig = SMSGlobalConfig{
Name: "SMSGlobal",
Enabled: c.SMS.Enabled,
Verbose: c.SMS.Verbose,
Username: c.SMS.Username,
Password: c.SMS.Password,
Contacts: c.SMS.Contacts,
}
// flush old SMS config
c.SMS = nil
} else {
c.Communications.SMSGlobalConfig = SMSGlobalConfig{
Name: "SMSGlobal",
From: c.Name,
Username: "main",
Password: "test",
Contacts: []SMSContact{
{
Name: "bob",
Number: "1234",
Enabled: false,
},
},
}
}
} else {
c.Communications.SMSGlobalConfig = SMSGlobalConfig{
Name: "SMSGlobal",
Username: "main",
Password: "test",
Contacts: []SMSContact{
{
Name: "bob",
Number: "1234",
Enabled: false,
},
},
}
}
} else {
if c.Communications.SMSGlobalConfig.From == "" {
c.Communications.SMSGlobalConfig.From = c.Name
}
if len(c.Communications.SMSGlobalConfig.From) > 11 {
log.Warnf(log.ConfigMgr, "SMSGlobal config supplied from name exceeds 11 characters, trimming.\n")
c.Communications.SMSGlobalConfig.From = c.Communications.SMSGlobalConfig.From[:11]
}
if c.SMS != nil {
// flush old SMS config
c.SMS = nil
}
}
if c.Communications.SMTPConfig.Name == "" {
c.Communications.SMTPConfig = SMTPConfig{
Name: "SMTP",
Host: "smtp.google.com",
Port: "537",
AccountName: "some",
AccountPassword: "password",
RecipientList: "lol123@gmail.com",
}
}
if c.Communications.TelegramConfig.Name == "" {
c.Communications.TelegramConfig = TelegramConfig{
Name: "Telegram",
VerificationToken: "testest",
}
}
if c.Communications.SlackConfig.Name != "Slack" ||
c.Communications.SMSGlobalConfig.Name != "SMSGlobal" ||
c.Communications.SMTPConfig.Name != "SMTP" ||
c.Communications.TelegramConfig.Name != "Telegram" {
log.Warnln(log.ConfigMgr, "Communications config name/s not set correctly")
}
if c.Communications.SlackConfig.Enabled {
if c.Communications.SlackConfig.TargetChannel == "" ||
c.Communications.SlackConfig.VerificationToken == "" ||
c.Communications.SlackConfig.VerificationToken == "testtest" {
c.Communications.SlackConfig.Enabled = false
log.Warnln(log.ConfigMgr, "Slack enabled in config but variable data not set, disabling.")
}
}
if c.Communications.SMSGlobalConfig.Enabled {
if c.Communications.SMSGlobalConfig.Username == "" ||
c.Communications.SMSGlobalConfig.Password == "" ||
len(c.Communications.SMSGlobalConfig.Contacts) == 0 {
c.Communications.SMSGlobalConfig.Enabled = false
log.Warnln(log.ConfigMgr, "SMSGlobal enabled in config but variable data not set, disabling.")
}
}
if c.Communications.SMTPConfig.Enabled {
if c.Communications.SMTPConfig.Host == "" ||
c.Communications.SMTPConfig.Port == "" ||
c.Communications.SMTPConfig.AccountName == "" ||
c.Communications.SMTPConfig.AccountPassword == "" {
c.Communications.SMTPConfig.Enabled = false
log.Warnln(log.ConfigMgr, "SMTP enabled in config but variable data not set, disabling.")
}
}
if c.Communications.TelegramConfig.Enabled {
if c.Communications.TelegramConfig.VerificationToken == "" {
c.Communications.TelegramConfig.Enabled = false
log.Warnln(log.ConfigMgr, "Telegram enabled in config but variable data not set, disabling.")
}
}
}
// GetExchangeAssetTypes returns the exchanges supported asset types
func (c *Config) GetExchangeAssetTypes(exchName string) (asset.Items, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
if exchCfg.CurrencyPairs == nil {
return nil, fmt.Errorf("exchange %s currency pairs is nil", exchName)
}
return exchCfg.CurrencyPairs.AssetTypes, nil
}
// SupportsExchangeAssetType returns whether or not the exchange supports the supplied asset type
func (c *Config) SupportsExchangeAssetType(exchName string, assetType asset.Item) (bool, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return false, err
}
if exchCfg.CurrencyPairs == nil {
return false, fmt.Errorf("exchange %s currency pairs is nil", exchName)
}
if !asset.IsValid(assetType) {
return false, fmt.Errorf("exchange %s invalid asset types", exchName)
}
return exchCfg.CurrencyPairs.AssetTypes.Contains(assetType), nil
}
// CheckExchangeAssetsConsistency checks the exchanges supported assets compared to the stored
// entries and removes any non supported
func (c *Config) CheckExchangeAssetsConsistency(exchName string) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return
}
exchangeAssetTypes, err := c.GetExchangeAssetTypes(exchName)
if err != nil {
return
}
storedAssetTypes := exchCfg.CurrencyPairs.GetAssetTypes()
for x := range storedAssetTypes {
if !exchangeAssetTypes.Contains(storedAssetTypes[x]) {
log.Warnf(log.ConfigMgr,
"%s has non-needed stored asset type %v. Removing..\n",
exchName, storedAssetTypes[x])
exchCfg.CurrencyPairs.Delete(storedAssetTypes[x])
}
}
}
// SetPairs sets the exchanges currency pairs
func (c *Config) SetPairs(exchName string, assetType asset.Item, enabled bool, pairs currency.Pairs) error {
if len(pairs) == 0 {
return fmt.Errorf("pairs is nil")
}
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return err
}
supports, err := c.SupportsExchangeAssetType(exchName, assetType)
if err != nil {
return err
}
if !supports {
return fmt.Errorf("exchange %s does not support asset type %v", exchName, assetType)
}
exchCfg.CurrencyPairs.StorePairs(assetType, pairs, enabled)
return nil
}
// GetCurrencyPairConfig returns currency pair config for the desired exchange and asset type
func (c *Config) GetCurrencyPairConfig(exchName string, assetType asset.Item) (*currency.PairStore, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
supports, err := c.SupportsExchangeAssetType(exchName, assetType)
if err != nil {
return nil, err
}
if !supports {
return nil, fmt.Errorf("exchange %s does not support asset type %v", exchName, assetType)
}
return exchCfg.CurrencyPairs.Get(assetType), nil
}
// CheckPairConfigFormats checks to see if the pair config format is valid
func (c *Config) CheckPairConfigFormats(exchName string) error {
assetTypes, err := c.GetExchangeAssetTypes(exchName)
if err != nil {
return err
}
for x := range assetTypes {
assetType := assetTypes[x]
pairFmt, err := c.GetPairFormat(exchName, assetType)
if err != nil {
return err
}
// No err checking is required as the above checks the same
// conditions
pairs, _ := c.GetCurrencyPairConfig(exchName, assetType)
if len(pairs.Available) == 0 || len(pairs.Enabled) == 0 {
continue
}
checker := func(enabled bool) error {
pairsType := "enabled"
loadedPairs := pairs.Enabled
if !enabled {
pairsType = "available"
loadedPairs = pairs.Available
}
for y := range loadedPairs {
if pairFmt.Delimiter != "" && pairFmt.Index != "" {
return fmt.Errorf(
"exchange %s %s %s cannot have an index and delimiter set at the same time",
exchName, pairsType, assetType)
}
if pairFmt.Delimiter != "" {
if !strings.Contains(loadedPairs[y].String(), pairFmt.Delimiter) {
return fmt.Errorf(
"exchange %s %s %s pairs does not contain delimiter",
exchName, pairsType, assetType)
}
}
if pairFmt.Index != "" {
if !strings.Contains(loadedPairs[y].String(), pairFmt.Index) {
return fmt.Errorf("exchange %s %s %s pairs does not contain an index",
exchName, pairsType, assetType)
}
}
}
return nil
}
err = checker(true)
if err != nil {
return err
}
err = checker(false)
if err != nil {
return err
}
}
return nil
}
// CheckPairConsistency checks to see if the enabled pair exists in the
// available pairs list
func (c *Config) CheckPairConsistency(exchName string) error {
assetTypes, err := c.GetExchangeAssetTypes(exchName)
if err != nil {
return err
}
for x := range assetTypes {
enabledPairs, err := c.GetEnabledPairs(exchName, assetTypes[x])
if err != nil {
return err
}
availPairs, _ := c.GetAvailablePairs(exchName, assetTypes[x])
if len(availPairs) == 0 {
continue
}
var pairs, pairsRemoved currency.Pairs
update := false
if len(enabledPairs) > 0 {
for x := range enabledPairs {
if !availPairs.Contains(enabledPairs[x], true) {
update = true
pairsRemoved = append(pairsRemoved, enabledPairs[x])
continue
}
pairs = append(pairs, enabledPairs[x])
}
} else {
update = true
}
if !update {
continue
}
if len(pairs) == 0 || len(enabledPairs) == 0 {
newPair := availPairs.GetRandomPair()
c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{newPair})
log.Warnf(log.ExchangeSys, "Exchange %s: [%v] No enabled pairs found in available pairs, randomly added %v pair.\n",
exchName, assetTypes[x], newPair)
continue
} else {
c.SetPairs(exchName, assetTypes[x], true, pairs)
}
log.Warnf(log.ExchangeSys, "Exchange %s: [%v] Removing enabled pair(s) %v from enabled pairs as it isn't an available pair.\n",
exchName, assetTypes[x], pairsRemoved.Strings())
}
return nil
}
// SupportsPair returns true or not whether the exchange supports the supplied
// pair
func (c *Config) SupportsPair(exchName string, p currency.Pair, assetType asset.Item) (bool, error) {
pairs, err := c.GetAvailablePairs(exchName, assetType)
if err != nil {
return false, err
}
return pairs.Contains(p, false), nil
}
// GetPairFormat returns the exchanges pair config storage format
func (c *Config) GetPairFormat(exchName string, assetType asset.Item) (currency.PairFormat, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return currency.PairFormat{}, err
}
supports, err := c.SupportsExchangeAssetType(exchName, assetType)
if err != nil {
return currency.PairFormat{}, err
}
if !supports {
return currency.PairFormat{},
fmt.Errorf("exchange %s does not support asset type %s", exchName,
assetType)
}
if exchCfg.CurrencyPairs.UseGlobalFormat {
return *exchCfg.CurrencyPairs.ConfigFormat, nil
}
p := exchCfg.CurrencyPairs.Get(assetType)
if p == nil {
return currency.PairFormat{},
fmt.Errorf("exchange %s pair store for asset type %s is nil", exchName,
assetType)
}
return *p.ConfigFormat, nil
}
// GetAvailablePairs returns a list of currency pairs for a specifc exchange
func (c *Config) GetAvailablePairs(exchName string, assetType asset.Item) (currency.Pairs, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
pairFormat, err := c.GetPairFormat(exchName, assetType)
if err != nil {
return nil, err
}
pairs := exchCfg.CurrencyPairs.GetPairs(assetType, false)
if pairs == nil {
return nil, nil
}
return pairs.Format(pairFormat.Delimiter, pairFormat.Index,
pairFormat.Uppercase), nil
}
// GetEnabledPairs returns a list of currency pairs for a specifc exchange
func (c *Config) GetEnabledPairs(exchName string, assetType asset.Item) ([]currency.Pair, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
pairFormat, err := c.GetPairFormat(exchName, assetType)
if err != nil {
return nil, err
}
pairs := exchCfg.CurrencyPairs.GetPairs(assetType, true)
if pairs == nil {
return nil, nil
}
return pairs.Format(pairFormat.Delimiter, pairFormat.Index,
pairFormat.Uppercase), nil
}
// GetEnabledExchanges returns a list of enabled exchanges
func (c *Config) GetEnabledExchanges() []string {
var enabledExchs []string
for i := range c.Exchanges {
if c.Exchanges[i].Enabled {
enabledExchs = append(enabledExchs, c.Exchanges[i].Name)
}
}
return enabledExchs
}
// GetDisabledExchanges returns a list of disabled exchanges
func (c *Config) GetDisabledExchanges() []string {
var disabledExchs []string
for i := range c.Exchanges {
if !c.Exchanges[i].Enabled {
disabledExchs = append(disabledExchs, c.Exchanges[i].Name)
}
}
return disabledExchs
}
// CountEnabledExchanges returns the number of exchanges that are enabled.
func (c *Config) CountEnabledExchanges() int {
counter := 0
for i := range c.Exchanges {
if c.Exchanges[i].Enabled {
counter++
}
}
return counter
}
// GetConfigCurrencyPairFormat returns the config currency pair format
// for a specific exchange
func (c *Config) GetConfigCurrencyPairFormat(exchName string) (*currency.PairFormat, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
return exchCfg.ConfigCurrencyPairFormat, nil
}
// GetRequestCurrencyPairFormat returns the request currency pair format
// for a specific exchange
func (c *Config) GetRequestCurrencyPairFormat(exchName string) (*currency.PairFormat, error) {
exchCfg, err := c.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
return exchCfg.RequestCurrencyPairFormat, nil
}
// GetCurrencyPairDisplayConfig retrieves the currency pair display preference
func (c *Config) GetCurrencyPairDisplayConfig() *CurrencyPairFormatConfig {
return c.Currency.CurrencyPairFormat
}
// GetAllExchangeConfigs returns all exchange configurations
func (c *Config) GetAllExchangeConfigs() []ExchangeConfig {
m.Lock()
defer m.Unlock()
return c.Exchanges
}
// GetExchangeConfig returns exchange configurations by its indivdual name
func (c *Config) GetExchangeConfig(name string) (*ExchangeConfig, error) {
m.Lock()
defer m.Unlock()
for i := range c.Exchanges {
if strings.EqualFold(c.Exchanges[i].Name, name) {
return &c.Exchanges[i], nil
}
}
return nil, fmt.Errorf(ErrExchangeNotFound, name)
}
// GetForexProviderConfig returns a forex provider configuration by its name
func (c *Config) GetForexProviderConfig(name string) (base.Settings, error) {
m.Lock()
defer m.Unlock()
for i := range c.Currency.ForexProviders {
if strings.EqualFold(c.Currency.ForexProviders[i].Name, name) {
return c.Currency.ForexProviders[i], nil
}
}
return base.Settings{}, errors.New("provider not found")
}
// GetForexProvidersConfig returns a list of available forex providers
func (c *Config) GetForexProvidersConfig() []base.Settings {
m.Lock()
defer m.Unlock()
return c.Currency.ForexProviders
}
// GetPrimaryForexProvider returns the primary forex provider
func (c *Config) GetPrimaryForexProvider() string {
m.Lock()
defer m.Unlock()
for i := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[i].PrimaryProvider {
return c.Currency.ForexProviders[i].Name
}
}
return ""
}
// UpdateExchangeConfig updates exchange configurations
func (c *Config) UpdateExchangeConfig(e *ExchangeConfig) error {
m.Lock()
defer m.Unlock()
for i := range c.Exchanges {
if strings.EqualFold(c.Exchanges[i].Name, e.Name) {
c.Exchanges[i] = *e
return nil
}
}
return fmt.Errorf(ErrExchangeNotFound, e.Name)
}
// CheckExchangeConfigValues returns configuation values for all enabled
// exchanges
func (c *Config) CheckExchangeConfigValues() error {
if len(c.Exchanges) == 0 {
return errors.New("no exchange configs found")
}
exchanges := 0
for i := range c.Exchanges {
if strings.EqualFold(c.Exchanges[i].Name, "GDAX") {
c.Exchanges[i].Name = "CoinbasePro"
}
// Check to see if the old API storage format is used
if c.Exchanges[i].APIKey != nil {
// It is, migrate settings to new format
c.Exchanges[i].API.AuthenticatedSupport = *c.Exchanges[i].AuthenticatedAPISupport
if c.Exchanges[i].AuthenticatedWebsocketAPISupport != nil {
c.Exchanges[i].API.AuthenticatedWebsocketSupport = *c.Exchanges[i].AuthenticatedWebsocketAPISupport
}
c.Exchanges[i].API.Credentials.Key = *c.Exchanges[i].APIKey
c.Exchanges[i].API.Credentials.Secret = *c.Exchanges[i].APISecret
if c.Exchanges[i].APIAuthPEMKey != nil {
c.Exchanges[i].API.Credentials.PEMKey = *c.Exchanges[i].APIAuthPEMKey
}
if c.Exchanges[i].APIAuthPEMKeySupport != nil {
c.Exchanges[i].API.PEMKeySupport = *c.Exchanges[i].APIAuthPEMKeySupport
}
if c.Exchanges[i].ClientID != nil {
c.Exchanges[i].API.Credentials.ClientID = *c.Exchanges[i].ClientID
}
if c.Exchanges[i].WebsocketURL != nil {
c.Exchanges[i].API.Endpoints.WebsocketURL = *c.Exchanges[i].WebsocketURL
}
c.Exchanges[i].API.Endpoints.URL = *c.Exchanges[i].APIURL
c.Exchanges[i].API.Endpoints.URLSecondary = *c.Exchanges[i].APIURLSecondary
// Flush settings
c.Exchanges[i].AuthenticatedAPISupport = nil
c.Exchanges[i].AuthenticatedWebsocketAPISupport = nil
c.Exchanges[i].APIKey = nil
c.Exchanges[i].APISecret = nil
c.Exchanges[i].ClientID = nil
c.Exchanges[i].APIAuthPEMKeySupport = nil
c.Exchanges[i].APIAuthPEMKey = nil
c.Exchanges[i].APIURL = nil
c.Exchanges[i].APIURLSecondary = nil
c.Exchanges[i].WebsocketURL = nil
}
if c.Exchanges[i].Features == nil {
c.Exchanges[i].Features = &FeaturesConfig{}
}
if c.Exchanges[i].SupportsAutoPairUpdates != nil {
c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates = *c.Exchanges[i].SupportsAutoPairUpdates
c.Exchanges[i].Features.Enabled.AutoPairUpdates = *c.Exchanges[i].SupportsAutoPairUpdates
c.Exchanges[i].SupportsAutoPairUpdates = nil
}
if c.Exchanges[i].Websocket != nil {
c.Exchanges[i].Features.Enabled.Websocket = *c.Exchanges[i].Websocket
c.Exchanges[i].Websocket = nil
}
if c.Exchanges[i].API.Endpoints.URL != APIURLNonDefaultMessage {
if c.Exchanges[i].API.Endpoints.URL == "" {
// Set default if nothing set
c.Exchanges[i].API.Endpoints.URL = APIURLNonDefaultMessage
}
}
if c.Exchanges[i].API.Endpoints.URLSecondary != APIURLNonDefaultMessage {
if c.Exchanges[i].API.Endpoints.URLSecondary == "" {
// Set default if nothing set
c.Exchanges[i].API.Endpoints.URLSecondary = APIURLNonDefaultMessage
}
}
if c.Exchanges[i].API.Endpoints.WebsocketURL != WebsocketURLNonDefaultMessage {
if c.Exchanges[i].API.Endpoints.WebsocketURL == "" {
c.Exchanges[i].API.Endpoints.WebsocketURL = WebsocketURLNonDefaultMessage
}
}
// Check if see if the new currency pairs format is empty and flesh it out if so
if c.Exchanges[i].CurrencyPairs == nil {
c.Exchanges[i].CurrencyPairs = new(currency.PairsManager)
c.Exchanges[i].CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
if c.Exchanges[i].PairsLastUpdated != nil {
c.Exchanges[i].CurrencyPairs.LastUpdated = *c.Exchanges[i].PairsLastUpdated
}
c.Exchanges[i].CurrencyPairs.ConfigFormat = c.Exchanges[i].ConfigCurrencyPairFormat
c.Exchanges[i].CurrencyPairs.RequestFormat = c.Exchanges[i].RequestCurrencyPairFormat
if c.Exchanges[i].AssetTypes == nil {
c.Exchanges[i].CurrencyPairs.AssetTypes = asset.Items{
asset.Spot,
}
} else {
c.Exchanges[i].CurrencyPairs.AssetTypes = asset.New(
strings.ToLower(*c.Exchanges[i].AssetTypes),
)
}
var availPairs, enabledPairs currency.Pairs
if c.Exchanges[i].AvailablePairs != nil {
availPairs = *c.Exchanges[i].AvailablePairs
}
if c.Exchanges[i].EnabledPairs != nil {
enabledPairs = *c.Exchanges[i].EnabledPairs
}
c.Exchanges[i].CurrencyPairs.UseGlobalFormat = true
c.Exchanges[i].CurrencyPairs.Store(asset.Spot,
currency.PairStore{
Available: availPairs,
Enabled: enabledPairs,
},
)
// flush old values
c.Exchanges[i].PairsLastUpdated = nil
c.Exchanges[i].ConfigCurrencyPairFormat = nil
c.Exchanges[i].RequestCurrencyPairFormat = nil
c.Exchanges[i].AssetTypes = nil
c.Exchanges[i].AvailablePairs = nil
c.Exchanges[i].EnabledPairs = nil
}
if c.Exchanges[i].Enabled {
if c.Exchanges[i].Name == "" {
log.Errorf(log.ConfigMgr, ErrExchangeNameEmpty, i)
c.Exchanges[i].Enabled = false
continue
}
if (c.Exchanges[i].API.AuthenticatedSupport || c.Exchanges[i].API.AuthenticatedWebsocketSupport) && c.Exchanges[i].API.CredentialsValidator != nil {
var failed bool
if c.Exchanges[i].API.CredentialsValidator.RequiresKey && (c.Exchanges[i].API.Credentials.Key == "" || c.Exchanges[i].API.Credentials.Key == DefaultAPIKey) {
failed = true
}
if c.Exchanges[i].API.CredentialsValidator.RequiresSecret && (c.Exchanges[i].API.Credentials.Secret == "" || c.Exchanges[i].API.Credentials.Secret == DefaultAPISecret) {
failed = true
}
if c.Exchanges[i].API.CredentialsValidator.RequiresClientID && (c.Exchanges[i].API.Credentials.ClientID == DefaultAPIClientID || c.Exchanges[i].API.Credentials.ClientID == "") {
failed = true
}
if failed {
c.Exchanges[i].API.AuthenticatedSupport = false
c.Exchanges[i].API.AuthenticatedWebsocketSupport = false
log.Warnf(log.ExchangeSys, WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name)
}
}
if !c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates && !c.Exchanges[i].Features.Supports.WebsocketCapabilities.AutoPairUpdates {
lastUpdated := convert.UnixTimestampToTime(c.Exchanges[i].CurrencyPairs.LastUpdated)
lastUpdated = lastUpdated.AddDate(0, 0, configPairsLastUpdatedWarningThreshold)
if lastUpdated.Unix() <= time.Now().Unix() {
log.Warnf(log.ExchangeSys, WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, configPairsLastUpdatedWarningThreshold)
}
}
if c.Exchanges[i].HTTPTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Timeout value not set, defaulting to %v.\n", c.Exchanges[i].Name, configDefaultHTTPTimeout)
c.Exchanges[i].HTTPTimeout = configDefaultHTTPTimeout
}
if c.Exchanges[i].HTTPRateLimiter != nil {
if c.Exchanges[i].HTTPRateLimiter.Authenticated.Duration < 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter authenticated duration set to negative value, defaulting to 0\n", c.Exchanges[i].Name)
c.Exchanges[i].HTTPRateLimiter.Authenticated.Duration = 0
}
if c.Exchanges[i].HTTPRateLimiter.Authenticated.Rate < 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter authenticated rate set to negative value, defaulting to 0\n", c.Exchanges[i].Name)
c.Exchanges[i].HTTPRateLimiter.Authenticated.Rate = 0
}
if c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Duration < 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter unauthenticated duration set to negative value, defaulting to 0\n", c.Exchanges[i].Name)
c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Duration = 0
}
if c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Rate < 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter unauthenticated rate set to negative value, defaulting to 0\n", c.Exchanges[i].Name)
c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Rate = 0
}
}
if c.Exchanges[i].WebsocketResponseCheckTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response check timeout value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketResponseCheckTimeout)
c.Exchanges[i].WebsocketResponseCheckTimeout = configDefaultWebsocketResponseCheckTimeout
}
if c.Exchanges[i].WebsocketResponseMaxLimit <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response max limit value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketResponseMaxLimit)
c.Exchanges[i].WebsocketResponseMaxLimit = configDefaultWebsocketResponseMaxLimit
}
if c.Exchanges[i].WebsocketTrafficTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response traffic timeout value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketTrafficTimeout)
c.Exchanges[i].WebsocketTrafficTimeout = configDefaultWebsocketTrafficTimeout
}
if c.Exchanges[i].WebsocketOrderbookBufferLimit <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketOrderbookBufferLimit)
c.Exchanges[i].WebsocketOrderbookBufferLimit = configDefaultWebsocketOrderbookBufferLimit
}
err := c.CheckPairConsistency(c.Exchanges[i].Name)
if err != nil {
log.Errorf(log.ExchangeSys, "Exchange %s: CheckPairConsistency error: %s\n", c.Exchanges[i].Name, err)
c.Exchanges[i].Enabled = false
continue
}
c.CheckExchangeAssetsConsistency(c.Exchanges[i].Name)
for x := range c.Exchanges[i].BankAccounts {
if !c.Exchanges[i].BankAccounts[x].Enabled {
continue
}
err := c.Exchanges[i].BankAccounts[x].Validate()
if err != nil {
c.Exchanges[i].BankAccounts[x].Enabled = false
log.Warn(log.ConfigMgr, err.Error())
}
}
exchanges++
}
}
if exchanges == 0 {
return errors.New(ErrNoEnabledExchanges)
}
return nil
}
// CheckCurrencyConfigValues checks to see if the currency config values are correct or not
func (c *Config) CheckCurrencyConfigValues() error {
fxProviders := forexprovider.GetAvailableForexProviders()
if len(fxProviders) == 0 {
return errors.New("no forex providers available")
}
if len(fxProviders) != len(c.Currency.ForexProviders) {
for x := range fxProviders {
_, err := c.GetForexProviderConfig(fxProviders[x])
if err != nil {
log.Warnf(log.Global, "%s forex provider not found, adding to config..\n", fxProviders[x])
c.Currency.ForexProviders = append(c.Currency.ForexProviders, base.Settings{
Name: fxProviders[x],
RESTPollingDelay: 600,
APIKey: DefaultUnsetAPIKey,
APIKeyLvl: -1,
})
}
}
}
count := 0
for i := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[i].Enabled {
if c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey && c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
log.Warnf(log.Global, "%s enabled forex provider API key not set. Please set this in your config.json file\n", c.Currency.ForexProviders[i].Name)
c.Currency.ForexProviders[i].Enabled = false
c.Currency.ForexProviders[i].PrimaryProvider = false
continue
}
if c.Currency.ForexProviders[i].Name == "CurrencyConverter" {
if c.Currency.ForexProviders[i].Enabled &&
c.Currency.ForexProviders[i].PrimaryProvider &&
(c.Currency.ForexProviders[i].APIKey == "" ||
c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey) {
log.Warnln(log.Global, "CurrencyConverter forex provider no longer supports unset API key requests. Switching to ExchangeRates FX provider..")
c.Currency.ForexProviders[i].Enabled = false
c.Currency.ForexProviders[i].PrimaryProvider = false
c.Currency.ForexProviders[i].APIKey = DefaultUnsetAPIKey
c.Currency.ForexProviders[i].APIKeyLvl = -1
continue
}
}
if c.Currency.ForexProviders[i].APIKeyLvl == -1 && c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
log.Warnf(log.Global, "%s APIKey Level not set, functions limited. Please set this in your config.json file\n",
c.Currency.ForexProviders[i].Name)
}
count++
}
}
if count == 0 {
for x := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[x].Name == DefaultForexProviderExchangeRatesAPI {
c.Currency.ForexProviders[x].Enabled = true
c.Currency.ForexProviders[x].PrimaryProvider = true
log.Warnln(log.ConfigMgr, "Using ExchangeRatesAPI for default forex provider.")
}
}
}
if c.Currency.CryptocurrencyProvider == (CryptocurrencyProvider{}) {
c.Currency.CryptocurrencyProvider.Name = "CoinMarketCap"
c.Currency.CryptocurrencyProvider.Enabled = false
c.Currency.CryptocurrencyProvider.Verbose = false
c.Currency.CryptocurrencyProvider.AccountPlan = DefaultUnsetAccountPlan
c.Currency.CryptocurrencyProvider.APIkey = DefaultUnsetAPIKey
}
if c.Currency.CryptocurrencyProvider.Enabled {
if c.Currency.CryptocurrencyProvider.APIkey == "" ||
c.Currency.CryptocurrencyProvider.APIkey == DefaultUnsetAPIKey {
log.Warnln(log.ConfigMgr, "CryptocurrencyProvider enabled but api key is unset please set this in your config.json file")
}
if c.Currency.CryptocurrencyProvider.AccountPlan == "" ||
c.Currency.CryptocurrencyProvider.AccountPlan == DefaultUnsetAccountPlan {
log.Warnln(log.ConfigMgr, "CryptocurrencyProvider enabled but account plan is unset please set this in your config.json file")
}
} else {
if c.Currency.CryptocurrencyProvider.APIkey == "" {
c.Currency.CryptocurrencyProvider.APIkey = DefaultUnsetAPIKey
}
if c.Currency.CryptocurrencyProvider.AccountPlan == "" {
c.Currency.CryptocurrencyProvider.AccountPlan = DefaultUnsetAccountPlan
}
}
if c.Currency.Cryptocurrencies.Join() == "" {
if c.Cryptocurrencies != nil {
c.Currency.Cryptocurrencies = *c.Cryptocurrencies
c.Cryptocurrencies = nil
} else {
c.Currency.Cryptocurrencies = currency.GetDefaultCryptocurrencies()
}
}
if c.Currency.CurrencyPairFormat == nil {
if c.CurrencyPairFormat != nil {
c.Currency.CurrencyPairFormat = c.CurrencyPairFormat
c.CurrencyPairFormat = nil
} else {
c.Currency.CurrencyPairFormat = &CurrencyPairFormatConfig{
Delimiter: "-",
Uppercase: true,
}
}
}
if c.Currency.FiatDisplayCurrency.IsEmpty() {
if c.FiatDisplayCurrency != nil {
c.Currency.FiatDisplayCurrency = *c.FiatDisplayCurrency
c.FiatDisplayCurrency = nil
} else {
c.Currency.FiatDisplayCurrency = currency.USD
}
}
// Flush old setting which still exists
if c.FiatDisplayCurrency != nil {
c.FiatDisplayCurrency = nil
}
return nil
}
// RetrieveConfigCurrencyPairs splits, assigns and verifies enabled currency
// pairs either cryptoCurrencies or fiatCurrencies
func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool, assetType asset.Item) error {
cryptoCurrencies := c.Currency.Cryptocurrencies
fiatCurrencies := currency.GetFiatCurrencies()
for x := range c.Exchanges {
if !c.Exchanges[x].Enabled && enabledOnly {
continue
}
supports, _ := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType)
if !supports {
continue
}
baseCurrencies := c.Exchanges[x].BaseCurrencies
for y := range baseCurrencies {
if !fiatCurrencies.Contains(baseCurrencies[y]) {
fiatCurrencies = append(fiatCurrencies, baseCurrencies[y])
}
}
}
for x := range c.Exchanges {
supports, _ := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType)
if !supports {
continue
}
var pairs []currency.Pair
var err error
if !c.Exchanges[x].Enabled && enabledOnly {
pairs, err = c.GetEnabledPairs(c.Exchanges[x].Name, assetType)
} else {
pairs, err = c.GetAvailablePairs(c.Exchanges[x].Name, assetType)
}
if err != nil {
return err
}
for y := range pairs {
if !fiatCurrencies.Contains(pairs[y].Base) &&
!cryptoCurrencies.Contains(pairs[y].Base) {
cryptoCurrencies = append(cryptoCurrencies, pairs[y].Base)
}
if !fiatCurrencies.Contains(pairs[y].Quote) &&
!cryptoCurrencies.Contains(pairs[y].Quote) {
cryptoCurrencies = append(cryptoCurrencies, pairs[y].Quote)
}
}
}
currency.UpdateCurrencies(fiatCurrencies, false)
currency.UpdateCurrencies(cryptoCurrencies, true)
return nil
}
// CheckLoggerConfig checks to see logger values are present and valid in config
// if not creates a default instance of the logger
func (c *Config) CheckLoggerConfig() error {
m.Lock()
defer m.Unlock()
if c.Logging.Enabled == nil || c.Logging.Output == "" {
c.Logging = log.GenDefaultSettings()
}
f := func(f bool) *bool { return &f }(false)
if c.Logging.LoggerFileConfig != nil {
if c.Logging.LoggerFileConfig.FileName == "" {
c.Logging.LoggerFileConfig.FileName = "log.txt"
}
if c.Logging.LoggerFileConfig.Rotate == nil {
c.Logging.LoggerFileConfig.Rotate = f
}
if c.Logging.LoggerFileConfig.MaxSize < 0 {
c.Logging.LoggerFileConfig.MaxSize = 100
}
log.FileLoggingConfiguredCorrectly = true
}
log.GlobalLogConfig = &c.Logging
logPath := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "logs")
err := common.CreateDir(logPath)
if err != nil {
return err
}
log.LogPath = logPath
return nil
}
func (c *Config) checkDatabaseConfig() error {
m.Lock()
defer m.Unlock()
if (c.Database == database.Config{}) {
c.Database.Driver = "sqlite"
c.Database.Database = database.DefaultSQLiteDatabase
}
if !c.Database.Enabled {
return nil
}
if !common.StringDataCompare(database.SupportedDrivers, c.Database.Driver) {
c.Database.Enabled = false
return fmt.Errorf("unsupported database driver %v, database disabled", c.Database.Driver)
}
if c.Database.Driver == "sqlite" {
databaseDir := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "/database")
err := common.CreateDir(databaseDir)
if err != nil {
return err
}
database.Conn.DataPath = databaseDir
}
database.Conn.Config = &c.Database
return nil
}
// CheckNTPConfig checks for missing or incorrectly configured NTPClient and recreates with known safe defaults
func (c *Config) CheckNTPConfig() {
m.Lock()
defer m.Unlock()
if c.NTPClient.AllowedDifference == nil || *c.NTPClient.AllowedDifference == 0 {
c.NTPClient.AllowedDifference = new(time.Duration)
*c.NTPClient.AllowedDifference = defaultNTPAllowedDifference
}
if c.NTPClient.AllowedNegativeDifference == nil || *c.NTPClient.AllowedNegativeDifference <= 0 {
c.NTPClient.AllowedNegativeDifference = new(time.Duration)
*c.NTPClient.AllowedNegativeDifference = defaultNTPAllowedNegativeDifference
}
if len(c.NTPClient.Pool) < 1 {
log.Warnln(log.ConfigMgr, "NTPClient enabled with no servers configured, enabling default pool.")
c.NTPClient.Pool = []string{"pool.ntp.org:123"}
}
}
// DisableNTPCheck allows the user to change how they are prompted for timesync alerts
func (c *Config) DisableNTPCheck(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")
var resp string
answered := false
for !answered {
answer, err := reader.ReadString('\n')
if err != nil {
return resp, err
}
answer = strings.TrimRight(answer, "\r\n")
switch answer {
case "a":
c.NTPClient.Level = 0
resp = "Time sync has been set to alert"
answered = true
case "w":
c.NTPClient.Level = 1
resp = "Time sync has been set to warn only"
answered = true
case "d":
c.NTPClient.Level = -1
resp = "Future notifications for out of time sync has been disabled"
answered = true
default:
log.Warnln(log.ConfigMgr,
"Invalid option selected, please try again (a)lert / (w)arn / (d)isable")
}
}
return resp, nil
}
// CheckConnectionMonitorConfig checks and if zero value assigns default values
func (c *Config) CheckConnectionMonitorConfig() {
m.Lock()
defer m.Unlock()
if c.ConnectionMonitor.CheckInterval == 0 {
c.ConnectionMonitor.CheckInterval = connchecker.DefaultCheckInterval
}
if len(c.ConnectionMonitor.DNSList) == 0 {
c.ConnectionMonitor.DNSList = connchecker.DefaultDNSList
}
if len(c.ConnectionMonitor.PublicDomainList) == 0 {
c.ConnectionMonitor.PublicDomainList = connchecker.DefaultDomainList
}
}
// GetFilePath returns the desired config file or the default config file name
// based on if the application is being run under test or normal mode.
func GetFilePath(file string) (string, error) {
if file != "" {
return file, nil
}
if flag.Lookup("test.v") != nil && !testBypass {
return ConfigTestFile, nil
}
exePath, err := common.GetExecutablePath()
if err != nil {
return "", err
}
oldDirs := []string{
filepath.Join(exePath, ConfigFile),
filepath.Join(exePath, EncryptedConfigFile),
}
newDir := common.GetDefaultDataDir(runtime.GOOS)
err = common.CreateDir(newDir)
if err != nil {
return "", err
}
newDirs := []string{
filepath.Join(newDir, ConfigFile),
filepath.Join(newDir, EncryptedConfigFile),
}
// First upgrade the old dir config file if it exists to the corresponding
// new one
for x := range oldDirs {
_, err := os.Stat(oldDirs[x])
if os.IsNotExist(err) {
continue
}
_, err = os.Stat(newDirs[x])
if !os.IsNotExist(err) {
log.Warnf(log.ConfigMgr,
"config.json file found in root dir and gct dir; cannot overwrite, defaulting to gct dir config.json at %s",
newDirs[x])
return newDirs[x], nil
}
if filepath.Ext(oldDirs[x]) == ".json" {
err = os.Rename(oldDirs[x], newDirs[0])
if err != nil {
return "", err
}
log.Debugf(log.ConfigMgr,
"Renamed old config file %s to %s\n",
oldDirs[x],
newDirs[0])
} else {
err = os.Rename(oldDirs[x], newDirs[1])
if err != nil {
return "", err
}
log.Debugf(log.ConfigMgr,
"Renamed old config file %s to %s\n",
oldDirs[x],
newDirs[1])
}
}
// Secondly check to see if the new config file extension is correct or not
for x := range newDirs {
_, err := os.Stat(newDirs[x])
if os.IsNotExist(err) {
continue
}
data, err := ioutil.ReadFile(newDirs[x])
if err != nil {
return "", err
}
if ConfirmECS(data) {
if filepath.Ext(newDirs[x]) == ".dat" {
return newDirs[x], nil
}
err = os.Rename(newDirs[x], newDirs[1])
if err != nil {
return "", err
}
return newDirs[1], nil
}
if filepath.Ext(newDirs[x]) == ".json" {
return newDirs[x], nil
}
err = os.Rename(newDirs[x], newDirs[0])
if err != nil {
return "", err
}
return newDirs[0], nil
}
return "", errors.New("config default file path error")
}
// ReadConfig verifies and checks for encryption and verifies the unencrypted
// file contains JSON.
func (c *Config) ReadConfig(configPath string, dryrun bool) error {
defaultPath, err := GetFilePath(configPath)
if err != nil {
return err
}
file, err := ioutil.ReadFile(defaultPath)
if err != nil {
return err
}
if !ConfirmECS(file) {
err = ConfirmConfigJSON(file, &c)
if err != nil {
return err
}
if c.EncryptConfig == configFileEncryptionDisabled {
return nil
}
if c.EncryptConfig == configFileEncryptionPrompt {
m.Lock()
IsInitialSetup = true
m.Unlock()
if c.PromptForConfigEncryption(configPath, dryrun) {
c.EncryptConfig = configFileEncryptionEnabled
return c.SaveConfig(defaultPath, dryrun)
}
}
} else {
errCounter := 0
for {
if errCounter >= configMaxAuthFailures {
return errors.New("failed to decrypt config after 3 attempts")
}
key, err := PromptForConfigKey(IsInitialSetup)
if err != nil {
log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err)
errCounter++
continue
}
var f []byte
f = append(f, file...)
data, err := DecryptConfigFile(f, key)
if err != nil {
log.Errorf(log.ConfigMgr, "DecryptConfigFile err: %s", err)
errCounter++
continue
}
err = ConfirmConfigJSON(data, &c)
if err != nil {
if errCounter < configMaxAuthFailures {
log.Error(log.ConfigMgr, "Invalid password.")
}
errCounter++
continue
}
break
}
}
return nil
}
// SaveConfig saves your configuration to your desired path
func (c *Config) SaveConfig(configPath string, dryrun bool) error {
if dryrun {
return nil
}
defaultPath, err := GetFilePath(configPath)
if err != nil {
return err
}
payload, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
if c.EncryptConfig == configFileEncryptionEnabled {
var key []byte
if IsInitialSetup {
key, err = PromptForConfigKey(true)
if err != nil {
return err
}
IsInitialSetup = false
}
payload, err = EncryptConfigFile(payload, key)
if err != nil {
return err
}
}
return common.WriteFile(defaultPath, payload)
}
// CheckRemoteControlConfig checks to see if the old c.Webserver field is used
// and migrates the existing settings to the new RemoteControl struct
func (c *Config) CheckRemoteControlConfig() {
m.Lock()
defer m.Unlock()
if c.Webserver != nil {
port := common.ExtractPort(c.Webserver.ListenAddress)
host := common.ExtractHost(c.Webserver.ListenAddress)
c.RemoteControl = RemoteControlConfig{
Username: c.Webserver.AdminUsername,
Password: c.Webserver.AdminPassword,
DeprecatedRPC: DepcrecatedRPCConfig{
Enabled: c.Webserver.Enabled,
ListenAddress: host + ":" + strconv.Itoa(port),
},
}
port++
c.RemoteControl.WebsocketRPC = WebsocketRPCConfig{
Enabled: c.Webserver.Enabled,
ListenAddress: host + ":" + strconv.Itoa(port),
ConnectionLimit: c.Webserver.WebsocketConnectionLimit,
MaxAuthFailures: c.Webserver.WebsocketMaxAuthFailures,
AllowInsecureOrigin: c.Webserver.WebsocketAllowInsecureOrigin,
}
port++
gRPCProxyPort := port + 1
c.RemoteControl.GRPC = GRPCConfig{
Enabled: c.Webserver.Enabled,
ListenAddress: host + ":" + strconv.Itoa(port),
GRPCProxyEnabled: c.Webserver.Enabled,
GRPCProxyListenAddress: host + ":" + strconv.Itoa(gRPCProxyPort),
}
// Then flush the old webserver settings
c.Webserver = nil
}
}
// CheckConfig checks all config settings
func (c *Config) CheckConfig() error {
err := c.CheckLoggerConfig()
if err != nil {
log.Errorf(log.ConfigMgr, "Failed to configure logger, some logging features unavailable: %s\n", err)
}
err = c.checkDatabaseConfig()
if err != nil {
log.Errorf(log.DatabaseMgr, "Failed to configure database: %v", err)
}
err = c.CheckExchangeConfigValues()
if err != nil {
return fmt.Errorf(ErrCheckingConfigValues, err)
}
c.CheckConnectionMonitorConfig()
c.CheckCommunicationsConfig()
c.CheckClientBankAccounts()
c.CheckRemoteControlConfig()
err = c.CheckCurrencyConfigValues()
if err != nil {
return err
}
if c.GlobalHTTPTimeout <= 0 {
log.Warnf(log.ConfigMgr, "Global HTTP Timeout value not set, defaulting to %v.\n", configDefaultHTTPTimeout)
c.GlobalHTTPTimeout = configDefaultHTTPTimeout
}
if c.NTPClient.Level != 0 {
c.CheckNTPConfig()
}
return nil
}
// LoadConfig loads your configuration file into your configuration object
func (c *Config) LoadConfig(configPath string, dryrun bool) error {
err := c.ReadConfig(configPath, dryrun)
if err != nil {
return fmt.Errorf(ErrFailureOpeningConfig, configPath, err)
}
return c.CheckConfig()
}
// UpdateConfig updates the config with a supplied config file
func (c *Config) UpdateConfig(configPath string, newCfg *Config, dryrun bool) error {
err := newCfg.CheckConfig()
if err != nil {
return err
}
c.Name = newCfg.Name
c.EncryptConfig = newCfg.EncryptConfig
c.Currency = newCfg.Currency
c.GlobalHTTPTimeout = newCfg.GlobalHTTPTimeout
c.Portfolio = newCfg.Portfolio
c.Communications = newCfg.Communications
c.Webserver = newCfg.Webserver
c.Exchanges = newCfg.Exchanges
err = c.SaveConfig(configPath, dryrun)
if err != nil {
return err
}
return c.LoadConfig(configPath, dryrun)
}
// GetConfig returns a pointer to a configuration object
func GetConfig() *Config {
return &Cfg
}