package config import ( "bufio" "encoding/json" "errors" "flag" "fmt" "io" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/connchecker" "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/forexprovider" "github.com/thrasher-/gocryptotrader/currency/forexprovider/base" log "github.com/thrasher-/gocryptotrader/logger" "github.com/thrasher-/gocryptotrader/portfolio" ) // 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 configMaxAuthFailres = 3 defaultNTPAllowedDifference = 50000000 defaultNTPAllowedNegativeDifference = 50000000 ) // Constants here hold some messages const ( ErrExchangeNameEmpty = "exchange #%d name is empty" ErrExchangeAvailablePairsEmpty = "exchange %s avaiable 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 ) // WebserverConfig struct holds the prestart variables for the webserver. type WebserverConfig struct { Enabled bool `json:"enabled"` AdminUsername string `json:"adminUsername"` AdminPassword string `json:"adminPassword"` ListenAddress string `json:"listenAddress"` WebsocketConnectionLimit int `json:"websocketConnectionLimit"` WebsocketMaxAuthFailures int `json:"websocketMaxAuthFailures"` WebsocketAllowInsecureOrigin bool `json:"websocketAllowInsecureOrigin"` } // Post holds the bot configuration data type Post struct { Data Config `json:"data"` } // CurrencyPairFormatConfig stores the users preferred currency pair display type CurrencyPairFormatConfig struct { Uppercase bool `json:"uppercase"` Delimiter string `json:"delimiter,omitempty"` Separator string `json:"separator,omitempty"` Index string `json:"index,omitempty"` } // Config is the overarching object that holds all the information for // prestart management of Portfolio, Communications, Webserver and Enabled // Exchanges type Config struct { Name string `json:"name"` EncryptConfig int `json:"encryptConfig"` GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` Logging log.Logging `json:"logging"` Profiler ProfilerConfig `json:"profiler"` NTPClient NTPClientConfig `json:"ntpclient"` Currency CurrencyConfig `json:"currencyConfig"` Communications CommunicationsConfig `json:"communications"` Portfolio portfolio.Base `json:"portfolioAddresses"` Webserver WebserverConfig `json:"webserver"` Exchanges []ExchangeConfig `json:"exchanges"` BankAccounts []BankAccount `json:"bankAccounts"` ConnectionMonitor ConnectionMonitorConfig `json:"connectionMonitor"` // Deprecated config settings, will be removed at a future date CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat,omitempty"` FiatDisplayCurrency currency.Code `json:"fiatDispayCurrency,omitempty"` Cryptocurrencies currency.Currencies `json:"cryptocurrencies,omitempty"` SMS *SMSGlobalConfig `json:"smsGlobal,omitempty"` } // ConnectionMonitorConfig defines the connection monitor variables to ensure // that there is internet connectivity type ConnectionMonitorConfig struct { DNSList []string `json:"preferredDNSList"` PublicDomainList []string `json:"preferredDomainList"` CheckInterval time.Duration `json:"checkInterval"` } // ProfilerConfig defines the profiler configuration to enable pprof type ProfilerConfig struct { Enabled bool `json:"enabled"` } // NTPClientConfig defines a network time protocol configuration to allow for // positive and negative differences type NTPClientConfig struct { Level int `json:"enabled"` Pool []string `json:"pool"` AllowedDifference *time.Duration `json:"allowedDifference"` AllowedNegativeDifference *time.Duration `json:"allowedNegativeDifference"` } // ExchangeConfig holds all the information needed for each enabled Exchange. type ExchangeConfig struct { Name string `json:"name"` Enabled bool `json:"enabled"` Verbose bool `json:"verbose"` Websocket bool `json:"websocket"` UseSandbox bool `json:"useSandbox"` RESTPollingDelay time.Duration `json:"restPollingDelay"` HTTPTimeout time.Duration `json:"httpTimeout"` HTTPUserAgent string `json:"httpUserAgent"` HTTPDebugging bool `json:"httpDebugging"` AuthenticatedAPISupport bool `json:"authenticatedApiSupport"` AuthenticatedWebsocketAPISupport bool `json:"authenticatedWebsocketApiSupport"` APIKey string `json:"apiKey"` APISecret string `json:"apiSecret"` APIAuthPEMKeySupport bool `json:"apiAuthPemKeySupport,omitempty"` APIAuthPEMKey string `json:"apiAuthPemKey,omitempty"` APIURL string `json:"apiUrl"` APIURLSecondary string `json:"apiUrlSecondary"` ProxyAddress string `json:"proxyAddress"` WebsocketURL string `json:"websocketUrl"` ClientID string `json:"clientId,omitempty"` AvailablePairs currency.Pairs `json:"availablePairs"` EnabledPairs currency.Pairs `json:"enabledPairs"` BaseCurrencies currency.Currencies `json:"baseCurrencies"` AssetTypes string `json:"assetTypes"` SupportsAutoPairUpdates bool `json:"supportsAutoPairUpdates"` PairsLastUpdated int64 `json:"pairsLastUpdated,omitempty"` ConfigCurrencyPairFormat *CurrencyPairFormatConfig `json:"configCurrencyPairFormat"` RequestCurrencyPairFormat *CurrencyPairFormatConfig `json:"requestCurrencyPairFormat"` BankAccounts []BankAccount `json:"bankAccounts"` } // BankAccount holds differing bank account details by supported funding // currency type BankAccount struct { Enabled bool `json:"enabled,omitempty"` BankName string `json:"bankName"` BankAddress string `json:"bankAddress"` AccountName string `json:"accountName"` AccountNumber string `json:"accountNumber"` SWIFTCode string `json:"swiftCode"` IBAN string `json:"iban"` BSBNumber string `json:"bsbNumber,omitempty"` SupportedCurrencies string `json:"supportedCurrencies"` SupportedExchanges string `json:"supportedExchanges,omitempty"` } // BankTransaction defines a related banking transaction type BankTransaction struct { ReferenceNumber string `json:"referenceNumber"` TransactionNumber string `json:"transactionNumber"` PaymentInstructions string `json:"paymentInstructions"` } // CurrencyConfig holds all the information needed for currency related manipulation type CurrencyConfig struct { ForexProviders []base.Settings `json:"forexProviders"` CryptocurrencyProvider CryptocurrencyProvider `json:"cryptocurrencyProvider"` Cryptocurrencies currency.Currencies `json:"cryptocurrencies"` CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat"` FiatDisplayCurrency currency.Code `json:"fiatDisplayCurrency"` CurrencyFileUpdateDuration time.Duration `json:"currencyFileUpdateDuration"` ForeignExchangeUpdateDuration time.Duration `json:"foreignExchangeUpdateDuration"` } // CryptocurrencyProvider defines coinmarketcap tools type CryptocurrencyProvider struct { Name string `json:"name"` Enabled bool `json:"enabled"` Verbose bool `json:"verbose"` APIkey string `json:"apiKey"` AccountPlan string `json:"accountPlan"` } // CommunicationsConfig holds all the information needed for each // enabled communication package type CommunicationsConfig struct { SlackConfig SlackConfig `json:"slack"` SMSGlobalConfig SMSGlobalConfig `json:"smsGlobal"` SMTPConfig SMTPConfig `json:"smtp"` TelegramConfig TelegramConfig `json:"telegram"` } // SlackConfig holds all variables to start and run the Slack package type SlackConfig struct { Name string `json:"name"` Enabled bool `json:"enabled"` Verbose bool `json:"verbose"` TargetChannel string `json:"targetChannel"` VerificationToken string `json:"verificationToken"` } // SMSContact stores the SMS contact info type SMSContact struct { Name string `json:"name"` Number string `json:"number"` Enabled bool `json:"enabled"` } // SMSGlobalConfig structure holds all the variables you need for instant // messaging and broadcast used by SMSGlobal type SMSGlobalConfig struct { Name string `json:"name"` Enabled bool `json:"enabled"` Verbose bool `json:"verbose"` Username string `json:"username"` Password string `json:"password"` Contacts []SMSContact `json:"contacts"` } // SMTPConfig holds all variables to start and run the SMTP package type SMTPConfig struct { Name string `json:"name"` Enabled bool `json:"enabled"` Verbose bool `json:"verbose"` Host string `json:"host"` Port string `json:"port"` AccountName string `json:"accountName"` AccountPassword string `json:"accountPassword"` RecipientList string `json:"recipientList"` } // TelegramConfig holds all variables to start and run the Telegram package type TelegramConfig struct { Name string `json:"name"` Enabled bool `json:"enabled"` Verbose bool `json:"verbose"` VerificationToken string `json:"verificationToken"` } // 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 common.StringContains(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 (common.StringContains(c.BankAccounts[x].SupportedExchanges, exchangeName) || c.BankAccounts[x].SupportedExchanges == "ALL") && common.StringContains(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() error { m.Lock() defer m.Unlock() if len(c.BankAccounts) == 0 { c.BankAccounts = append(c.BankAccounts, BankAccount{ BankName: "test", BankAddress: "test", AccountName: "TestAccount", AccountNumber: "0234", SWIFTCode: "91272837", IBAN: "98218738671897", SupportedCurrencies: "USD", SupportedExchanges: "ANX,Kraken", }, ) return nil } for i := range c.BankAccounts { if c.BankAccounts[i].Enabled { if c.BankAccounts[i].BankName == "" || c.BankAccounts[i].BankAddress == "" { return fmt.Errorf("banking details for %s is enabled but variables not set correctly", c.BankAccounts[i].BankName) } if c.BankAccounts[i].AccountName == "" || c.BankAccounts[i].AccountNumber == "" { return fmt.Errorf("banking account details for %s variables not set correctly", c.BankAccounts[i].BankName) } if c.BankAccounts[i].IBAN == "" && c.BankAccounts[i].SWIFTCode == "" && c.BankAccounts[i].BSBNumber == "" { return fmt.Errorf("critical banking numbers not set for %s in %s account", c.BankAccounts[i].BankName, c.BankAccounts[i].AccountName) } if c.BankAccounts[i].SupportedExchanges == "" { c.BankAccounts[i].SupportedExchanges = "ALL" } } } return nil } // 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", 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.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.Warn("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.Warn("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.Warn("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.Warn("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.Warn("Telegram enabled in config but variable data not set, disabling.") } } } // CheckPairConsistency checks to see if the enabled pair exists in the // available pairs list func (c *Config) CheckPairConsistency(exchName string) error { enabledPairs, err := c.GetEnabledPairs(exchName) if err != nil { return err } availPairs, err := c.GetAvailablePairs(exchName) if err != nil { return err } var pairs, pairsRemoved currency.Pairs update := false for x := range enabledPairs { if !availPairs.Contains(enabledPairs[x], true) { update = true pairsRemoved = append(pairsRemoved, enabledPairs[x]) continue } pairs = append(pairs, enabledPairs[x]) } if !update { return nil } exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return err } if len(pairs) == 0 { exchCfg.EnabledPairs = append(exchCfg.EnabledPairs, availPairs.GetRandomPair()) log.Debugf("Exchange %s: No enabled pairs found in available pairs, randomly added %v\n", exchName, exchCfg.EnabledPairs) } else { exchCfg.EnabledPairs = pairs } err = c.UpdateExchangeConfig(&exchCfg) if err != nil { return err } log.Debugf("Exchange %s: Removing enabled pair(s) %v from enabled pairs as it isn't an available pair", exchName, 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) (bool, error) { pairs, err := c.GetAvailablePairs(exchName) if err != nil { return false, err } return pairs.Contains(p, false), nil } // GetAvailablePairs returns a list of currency pairs for a specifc exchange func (c *Config) GetAvailablePairs(exchName string) (currency.Pairs, error) { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return nil, err } return exchCfg.AvailablePairs.Format(exchCfg.ConfigCurrencyPairFormat.Delimiter, exchCfg.ConfigCurrencyPairFormat.Index, exchCfg.ConfigCurrencyPairFormat.Uppercase), nil } // GetEnabledPairs returns a list of currency pairs for a specifc exchange func (c *Config) GetEnabledPairs(exchName string) (currency.Pairs, error) { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return nil, err } return exchCfg.EnabledPairs.Format(exchCfg.ConfigCurrencyPairFormat.Delimiter, exchCfg.ConfigCurrencyPairFormat.Index, exchCfg.ConfigCurrencyPairFormat.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) (*CurrencyPairFormatConfig, 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) (*CurrencyPairFormatConfig, 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 ExchangeConfig{}, 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") } // 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 { exchanges := 0 for i := range c.Exchanges { if strings.EqualFold(c.Exchanges[i].Name, "GDAX") { c.Exchanges[i].Name = "CoinbasePro" } if c.Exchanges[i].WebsocketURL != WebsocketURLNonDefaultMessage { if c.Exchanges[i].WebsocketURL == "" { c.Exchanges[i].WebsocketURL = WebsocketURLNonDefaultMessage } } if c.Exchanges[i].APIURL != APIURLNonDefaultMessage { if c.Exchanges[i].APIURL == "" { // Set default if nothing set c.Exchanges[i].APIURL = APIURLNonDefaultMessage } } if c.Exchanges[i].APIURLSecondary != APIURLNonDefaultMessage { if c.Exchanges[i].APIURLSecondary == "" { // Set default if nothing set c.Exchanges[i].APIURLSecondary = APIURLNonDefaultMessage } } if c.Exchanges[i].Enabled { if c.Exchanges[i].Name == "" { return fmt.Errorf(ErrExchangeNameEmpty, i) } if len(c.Exchanges[i].AvailablePairs) == 0 { return fmt.Errorf(ErrExchangeAvailablePairsEmpty, c.Exchanges[i].Name) } if len(c.Exchanges[i].EnabledPairs) == 0 { return fmt.Errorf(ErrExchangeEnabledPairsEmpty, c.Exchanges[i].Name) } if len(c.Exchanges[i].BaseCurrencies) == 0 { return fmt.Errorf(ErrExchangeBaseCurrenciesEmpty, c.Exchanges[i].Name) } var areAuthenticatedCredentialsValid bool if c.Exchanges[i].AuthenticatedWebsocketAPISupport || c.Exchanges[i].AuthenticatedAPISupport { areAuthenticatedCredentialsValid = c.areAuthenticatedCredentialsValid(i) } if c.Exchanges[i].AuthenticatedWebsocketAPISupport { c.Exchanges[i].AuthenticatedWebsocketAPISupport = areAuthenticatedCredentialsValid } if c.Exchanges[i].AuthenticatedAPISupport { c.Exchanges[i].AuthenticatedAPISupport = areAuthenticatedCredentialsValid } if !c.Exchanges[i].SupportsAutoPairUpdates { lastUpdated := common.UnixTimestampToTime(c.Exchanges[i].PairsLastUpdated) lastUpdated = lastUpdated.AddDate(0, 0, configPairsLastUpdatedWarningThreshold) if lastUpdated.Unix() <= time.Now().Unix() { log.Warnf(WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, configPairsLastUpdatedWarningThreshold) } } if c.Exchanges[i].HTTPTimeout <= 0 { log.Warnf("Exchange %s HTTP Timeout value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultHTTPTimeout) c.Exchanges[i].HTTPTimeout = configDefaultHTTPTimeout } err := c.CheckPairConsistency(c.Exchanges[i].Name) if err != nil { log.Errorf("Exchange %s: CheckPairConsistency error: %s", c.Exchanges[i].Name, err) } if len(c.Exchanges[i].BankAccounts) == 0 { c.Exchanges[i].BankAccounts = append(c.Exchanges[i].BankAccounts, BankAccount{}) } else { for y := range c.Exchanges[i].BankAccounts { bankAccount := &c.Exchanges[i].BankAccounts[y] if bankAccount.Enabled { if bankAccount.BankName == "" || bankAccount.BankAddress == "" { log.Warnf("banking details for %s is enabled but variables not set", c.Exchanges[i].Name) bankAccount.Enabled = false } if bankAccount.AccountName == "" || bankAccount.AccountNumber == "" { log.Warnf("banking account details for %s variables not set", c.Exchanges[i].Name) bankAccount.Enabled = false } if bankAccount.SupportedCurrencies == "" { log.Warnf("banking account details for %s acceptable funding currencies not set", c.Exchanges[i].Name) bankAccount.Enabled = false } if bankAccount.BSBNumber == "" && bankAccount.IBAN == "" && bankAccount.SWIFTCode == "" { log.Warnf("banking account details for %s critical banking numbers not set", c.Exchanges[i].Name) bankAccount.Enabled = false } } } } exchanges++ } } if exchanges == 0 { return errors.New(ErrNoEnabledExchanges) } return nil } func (c *Config) areAuthenticatedCredentialsValid(i int) bool { if c.Exchanges == nil { log.Error("Config: Failed to check exchange authenticated credentials due to c.Exchanges not setup") return false } if i < 0 || c.Exchanges == nil || len(c.Exchanges) < i { log.Error("Config: Failed to check exchange authenticated credentials due to invalid index") return false } resp := true if c.Exchanges[i].APIKey == "" || c.Exchanges[i].APIKey == DefaultUnsetAPIKey { resp = false } if (c.Exchanges[i].APISecret == "" || c.Exchanges[i].APISecret == DefaultUnsetAPISecret) && c.Exchanges[i].Name != "COINUT" { resp = false } if (c.Exchanges[i].ClientID == "ClientID" || c.Exchanges[i].ClientID == "") && (c.Exchanges[i].Name == "ITBIT" || c.Exchanges[i].Name == "Bitstamp" || c.Exchanges[i].Name == "COINUT" || c.Exchanges[i].Name == "CoinbasePro") { resp = false } // non-fatal error if !resp { log.Warnf(WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name) } return resp } // CheckWebserverConfigValues checks information before webserver starts and // returns an error if values are incorrect. func (c *Config) CheckWebserverConfigValues() error { if c.Webserver.AdminUsername == "" || c.Webserver.AdminPassword == "" { return errors.New(WarningWebserverCredentialValuesEmpty) } if !common.StringContains(c.Webserver.ListenAddress, ":") { return errors.New(WarningWebserverListenAddressInvalid) } portStr := common.SplitStrings(c.Webserver.ListenAddress, ":")[1] port, err := strconv.Atoi(portStr) if err != nil { return errors.New(WarningWebserverListenAddressInvalid) } if port < 1 || port > 65355 { return errors.New(WarningWebserverListenAddressInvalid) } if c.Webserver.WebsocketConnectionLimit <= 0 { c.Webserver.WebsocketConnectionLimit = 1 } if c.Webserver.WebsocketMaxAuthFailures <= 0 { c.Webserver.WebsocketMaxAuthFailures = 3 } 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("%s forex provider not found, adding to config..", 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("%s enabled forex provider API key not set. Please set this in your config.json file", 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.Warnf("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("%s APIKey Level not set, functions limited. Please set this in your config.json file", 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.Warn("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.Warnf("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.Warnf("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.Join() != "" { 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.IsEmpty() { c.Currency.FiatDisplayCurrency = c.FiatDisplayCurrency c.FiatDisplayCurrency = currency.NewCode("") } else { c.Currency.FiatDisplayCurrency = currency.USD } } return nil } // RetrieveConfigCurrencyPairs splits, assigns and verifies enabled currency // pairs either cryptoCurrencies or fiatCurrencies func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool) error { cryptoCurrencies := c.Currency.Cryptocurrencies fiatCurrencies := currency.GetFiatCurrencies() for x := range c.Exchanges { if !c.Exchanges[x].Enabled && enabledOnly { 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 { var pairs []currency.Pair var err error if !c.Exchanges[x].Enabled && enabledOnly { pairs, err = c.GetEnabledPairs(c.Exchanges[x].Name) } else { pairs, err = c.GetAvailablePairs(c.Exchanges[x].Name) } 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() // check if enabled is nil or level is a blank string if c.Logging.Enabled == nil || c.Logging.Level == "" { // Creates a new pointer to bool and sets it as true t := func(t bool) *bool { return &t }(true) log.Warn("Missing or invalid config settings using safe defaults") // Set logger to safe defaults c.Logging = log.Logging{ Enabled: t, Level: "DEBUG|INFO|WARN|ERROR|FATAL", ColourOutput: false, File: "debug.txt", Rotate: false, } log.Logger = &c.Logging } else { log.Logger = &c.Logging } if len(c.Logging.File) > 0 { logPath := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "logs") err := common.CreateDir(logPath) if err != nil { return err } log.LogPath = logPath } 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.Warn("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.Warn("Your system time is out of sync this may cause issues with trading") log.Warn("How would you like to show future notifications? (a)lert / (w)arn / (d)isable \n") var answered = false for ok := true; ok; ok = (!answered) { answer, err := reader.ReadString('\n') if err != nil { return "", err } answer = strings.TrimRight(answer, "\r\n") switch answer { case "a": c.NTPClient.Level = 0 answered = true return "Time sync has been set to alert", nil case "w": c.NTPClient.Level = 1 answered = true return "Time sync has been set to warn only", nil case "d": c.NTPClient.Level = -1 answered = true return "Future notications for out time sync have been disabled", nil } } return "", errors.New("something went wrong NTPCheck should never make it this far") } // 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 } oldDir := exePath + common.GetOSPathSlash() oldDirs := []string{oldDir + ConfigFile, oldDir + EncryptedConfigFile} newDir := common.GetDefaultDataDir(runtime.GOOS) + common.GetOSPathSlash() err = common.CreateDir(newDir) if err != nil { return "", err } newDirs := []string{newDir + ConfigFile, 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 } if filepath.Ext(oldDirs[x]) == ".json" { err = os.Rename(oldDirs[x], newDirs[0]) if err != nil { return "", err } log.Debugf("Renamed old config file %s to %s", oldDirs[x], newDirs[0]) } else { err = os.Rename(oldDirs[x], newDirs[1]) if err != nil { return "", err } log.Debugf("Renamed old config file %s to %s", 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 := common.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) error { defaultPath, err := GetFilePath(configPath) if err != nil { return err } file, err := common.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() { c.EncryptConfig = configFileEncryptionEnabled return c.SaveConfig(defaultPath) } } } else { errCounter := 0 for { if errCounter >= configMaxAuthFailres { return errors.New("failed to decrypt config after 3 attempts") } key, err := PromptForConfigKey(IsInitialSetup) if err != nil { log.Errorf("PromptForConfigKey err: %s", err) errCounter++ continue } var f []byte f = append(f, file...) data, err := DecryptConfigFile(f, key) if err != nil { log.Errorf("DecryptConfigFile err: %s", err) errCounter++ continue } err = ConfirmConfigJSON(data, &c) if err != nil { if errCounter < configMaxAuthFailres { log.Errorf("Invalid password.") } errCounter++ continue } break } } return nil } // SaveConfig saves your configuration to your desired path func (c *Config) SaveConfig(configPath string) error { 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) } // CheckConfig checks all config settings func (c *Config) CheckConfig() error { err := c.CheckExchangeConfigValues() if err != nil { return fmt.Errorf(ErrCheckingConfigValues, err) } c.CheckConnectionMonitorConfig() c.CheckCommunicationsConfig() if c.Webserver.Enabled { err = c.CheckWebserverConfigValues() if err != nil { log.Warnf(ErrCheckingConfigValues, err) c.Webserver.Enabled = false } } err = c.CheckCurrencyConfigValues() if err != nil { return err } if c.GlobalHTTPTimeout <= 0 { log.Warnf("Global HTTP Timeout value not set, defaulting to %v.", configDefaultHTTPTimeout) c.GlobalHTTPTimeout = configDefaultHTTPTimeout } return c.CheckClientBankAccounts() } // LoadConfig loads your configuration file into your configuration object func (c *Config) LoadConfig(configPath string) error { err := c.ReadConfig(configPath) 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) 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) if err != nil { return err } return c.LoadConfig(configPath) } // GetConfig returns a pointer to a configuration object func GetConfig() *Config { return &Cfg }