package config import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "runtime" "strconv" "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/file" "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/connchecker" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctscript "github.com/thrasher-corp/gocryptotrader/gctscript/vm" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/portfolio/banking" ) // 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, id, depositingCurrency string) (*banking.Account, 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.EqualFold(c.Exchanges[x].BankAccounts[y].ID, id) { if common.StringDataCompareInsensitive( strings.Split(c.Exchanges[x].BankAccounts[y].SupportedCurrencies, ","), depositingCurrency) { return &c.Exchanges[x].BankAccounts[y], nil } } } } } return nil, 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 []banking.Account) 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) (*banking.Account, 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 nil, 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 *banking.Account) 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, banking.Account{ ID: "test-bank-01", 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: "Kraken,Bitstamp", }, ) 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() base.CommunicationsConfig { m.Lock() comms := c.Communications m.Unlock() return comms } // UpdateCommunicationsConfig sets a new updated version of a Communications // configuration func (c *Config) UpdateCommunicationsConfig(config *base.CommunicationsConfig) { m.Lock() c.Communications = *config m.Unlock() } // GetCryptocurrencyProviderConfig returns the communications configuration func (c *Config) GetCryptocurrencyProviderConfig() CryptocurrencyProvider { m.Lock() provider := c.Currency.CryptocurrencyProvider m.Unlock() return provider } // 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 = base.SlackConfig{ Name: "Slack", TargetChannel: "general", VerificationToken: "testtest", } } if c.Communications.SMSGlobalConfig.Name == "" { if c.SMS != nil { if c.SMS.Contacts != nil { c.Communications.SMSGlobalConfig = base.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 = base.SMSGlobalConfig{ Name: "SMSGlobal", From: c.Name, Username: "main", Password: "test", Contacts: []base.SMSContact{ { Name: "bob", Number: "1234", Enabled: false, }, }, } } } else { c.Communications.SMSGlobalConfig = base.SMSGlobalConfig{ Name: "SMSGlobal", Username: "main", Password: "test", Contacts: []base.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 = base.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 = base.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.GetAssetTypes(), nil } // SupportsExchangeAssetType returns whether or not the exchange supports the supplied asset type func (c *Config) SupportsExchangeAssetType(exchName string, assetType asset.Item) error { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return err } if exchCfg.CurrencyPairs == nil { return fmt.Errorf("exchange %s currency pairs is nil", exchName) } if !assetType.IsValid() { return fmt.Errorf("exchange %s invalid asset type %s", exchName, assetType) } if !exchCfg.CurrencyPairs.GetAssetTypes().Contains(assetType) { return fmt.Errorf("exchange %s unsupported asset type %s", exchName, assetType) } return nil } // SetPairs sets the exchanges currency pairs func (c *Config) SetPairs(exchName string, assetType asset.Item, enabled bool, pairs currency.Pairs) error { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return err } err = c.SupportsExchangeAssetType(exchName, assetType) if err != nil { return err } 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 } err = c.SupportsExchangeAssetType(exchName, assetType) if err != nil { return nil, err } return exchCfg.CurrencyPairs.Get(assetType) } // 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 } var atLeastOneEnabled bool for x := range assetTypes { enabledPairs, err := c.GetEnabledPairs(exchName, assetTypes[x]) if err == nil { if len(enabledPairs) != 0 { atLeastOneEnabled = true continue } var enabled bool enabled, err = c.AssetTypeEnabled(assetTypes[x], exchName) if err != nil { return err } if !enabled { continue } var availPairs currency.Pairs availPairs, err = c.GetAvailablePairs(exchName, assetTypes[x]) if err != nil { return err } err = c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{availPairs.GetRandomPair()}) if err != nil { return err } atLeastOneEnabled = true continue } // On error an enabled pair is not found in the available pairs list // so remove and report availPairs, err := c.GetAvailablePairs(exchName, assetTypes[x]) if err != nil { return err } var pairs, pairsRemoved currency.Pairs for x := range enabledPairs { if !availPairs.Contains(enabledPairs[x], true) { pairsRemoved = append(pairsRemoved, enabledPairs[x]) continue } pairs = append(pairs, enabledPairs[x]) } if len(pairsRemoved) == 0 { return fmt.Errorf("check pair consistency fault for asset %s, conflict found but no pairs removed", assetTypes[x]) } // Flush corrupted/misspelled enabled pairs in config err = c.SetPairs(exchName, assetTypes[x], true, pairs) if err != nil { return err } log.Warnf(log.ConfigMgr, "Exchange %s: [%v] Removing enabled pair(s) %v from enabled pairs list, as it isn't located in the available pairs list.\n", exchName, assetTypes[x], pairsRemoved.Strings()) if len(pairs) != 0 { atLeastOneEnabled = true continue } enabled, err := c.AssetTypeEnabled(assetTypes[x], exchName) if err != nil { return err } if !enabled { continue } err = c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{availPairs.GetRandomPair()}) if err != nil { return err } atLeastOneEnabled = true } // If no pair is enabled across the entire range of assets, then atleast // enable one and turn on the asset type if !atLeastOneEnabled { avail, err := c.GetAvailablePairs(exchName, assetTypes[0]) if err != nil { return err } newPair := avail.GetRandomPair() err = c.SetPairs(exchName, assetTypes[0], true, currency.Pairs{newPair}) if err != nil { return err } log.Warnf(log.ConfigMgr, "Exchange %s: [%v] No enabled pairs found in available pairs list, randomly added %v pair.\n", exchName, assetTypes[0], newPair) } 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 { pairs, err := c.GetAvailablePairs(exchName, assetType) if err != nil { return false } return pairs.Contains(p, false) } // 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 } err = c.SupportsExchangeAssetType(exchName, assetType) if err != nil { return currency.PairFormat{}, err } if exchCfg.CurrencyPairs.UseGlobalFormat { return *exchCfg.CurrencyPairs.ConfigFormat, nil } p, err := exchCfg.CurrencyPairs.Get(assetType) if err != nil { return currency.PairFormat{}, err } if p == nil { return currency.PairFormat{}, fmt.Errorf("exchange %s pair store for asset type %s is nil", exchName, assetType) } if p.ConfigFormat == nil { return currency.PairFormat{}, fmt.Errorf("exchange %s pair config format 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, err := exchCfg.CurrencyPairs.GetPairs(assetType, false) if err != nil { return nil, err } 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.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, err := exchCfg.CurrencyPairs.GetPairs(assetType, true) if err != nil { return pairs, err } 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 } // 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() configs := c.Exchanges m.Unlock() return configs } // 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("%s %w", name, ErrExchangeNotFound) } // GetForexProvider returns a forex provider configuration by its name func (c *Config) GetForexProvider(name string) (currency.FXSettings, 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 currency.FXSettings{}, errors.New("provider not found") } // GetForexProviders returns a list of available forex providers func (c *Config) GetForexProviders() []currency.FXSettings { m.Lock() fxProviders := c.Currency.ForexProviders m.Unlock() return fxProviders } // 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("%s %w", e.Name, ErrExchangeNotFound) } // 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 } // 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 } // 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 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{ AssetEnabled: convert.BoolPtr(true), 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 } else { assets := c.Exchanges[i].CurrencyPairs.GetAssetTypes() var atLeastOne bool for index := range assets { err := c.Exchanges[i].CurrencyPairs.IsAssetEnabled(assets[index]) if err != nil { // Checks if we have an old config without the ability to // enable disable the entire asset if err.Error() == "cannot ascertain if asset is enabled, variable is nil" { log.Warnf(log.ConfigMgr, "Exchange %s: upgrading config for asset type %s and setting enabled.\n", c.Exchanges[i].Name, assets[index]) err = c.Exchanges[i].CurrencyPairs.SetAssetEnabled(assets[index], true) if err != nil { return err } atLeastOne = true } continue } atLeastOne = true } if !atLeastOne { if len(assets) == 0 { c.Exchanges[i].Enabled = false log.Warnf(log.ConfigMgr, "%s no assets found, disabling...", c.Exchanges[i].Name) continue } // turn on an asset if all disabled log.Warnf(log.ConfigMgr, "%s assets disabled, turning on asset %s", c.Exchanges[i].Name, assets[0]) err := c.Exchanges[i].CurrencyPairs.SetAssetEnabled(assets[0], true) if err != nil { return err } } } 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.ConfigMgr, 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, pairsLastUpdatedWarningThreshold) if lastUpdated.Unix() <= time.Now().Unix() { log.Warnf(log.ConfigMgr, WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, pairsLastUpdatedWarningThreshold) } } if c.Exchanges[i].HTTPTimeout <= 0 { log.Warnf(log.ConfigMgr, "Exchange %s HTTP Timeout value not set, defaulting to %v.\n", c.Exchanges[i].Name, defaultHTTPTimeout) c.Exchanges[i].HTTPTimeout = defaultHTTPTimeout } if c.Exchanges[i].WebsocketResponseCheckTimeout <= 0 { log.Warnf(log.ConfigMgr, "Exchange %s Websocket response check timeout value not set, defaulting to %v.", c.Exchanges[i].Name, defaultWebsocketResponseCheckTimeout) c.Exchanges[i].WebsocketResponseCheckTimeout = defaultWebsocketResponseCheckTimeout } if c.Exchanges[i].WebsocketResponseMaxLimit <= 0 { log.Warnf(log.ConfigMgr, "Exchange %s Websocket response max limit value not set, defaulting to %v.", c.Exchanges[i].Name, defaultWebsocketResponseMaxLimit) c.Exchanges[i].WebsocketResponseMaxLimit = defaultWebsocketResponseMaxLimit } if c.Exchanges[i].WebsocketTrafficTimeout <= 0 { log.Warnf(log.ConfigMgr, "Exchange %s Websocket response traffic timeout value not set, defaulting to %v.", c.Exchanges[i].Name, defaultWebsocketTrafficTimeout) c.Exchanges[i].WebsocketTrafficTimeout = defaultWebsocketTrafficTimeout } if c.Exchanges[i].OrderbookConfig.WebsocketBufferLimit <= 0 { log.Warnf(log.ConfigMgr, "Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.", c.Exchanges[i].Name, defaultWebsocketOrderbookBufferLimit) c.Exchanges[i].OrderbookConfig.WebsocketBufferLimit = defaultWebsocketOrderbookBufferLimit } err := c.CheckPairConsistency(c.Exchanges[i].Name) if err != nil { log.Errorf(log.ConfigMgr, "Exchange %s: CheckPairConsistency error: %s\n", c.Exchanges[i].Name, err) c.Exchanges[i].Enabled = false continue } 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.Warnln(log.ConfigMgr, err.Error()) } } exchanges++ } } if exchanges == 0 { return errors.New(ErrNoEnabledExchanges) } return nil } // CheckBankAccountConfig checks all bank accounts to see if they are valid func (c *Config) CheckBankAccountConfig() { for x := range c.BankAccounts { if c.BankAccounts[x].Enabled { err := c.BankAccounts[x].Validate() if err != nil { c.BankAccounts[x].Enabled = false log.Warn(log.ConfigMgr, err.Error()) } } } banking.Accounts = c.BankAccounts } // CheckCurrencyConfigValues checks to see if the currency config values are correct or not func (c *Config) CheckCurrencyConfigValues() error { fxProviders := forexprovider.GetSupportedForexProviders() if len(fxProviders) != len(c.Currency.ForexProviders) { for x := range fxProviders { _, err := c.GetForexProvider(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, currency.FXSettings{ 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].Name == "CurrencyConverter" || c.Currency.ForexProviders[i].Name == "ExchangeRates") && c.Currency.ForexProviders[i].PrimaryProvider && (c.Currency.ForexProviders[i].APIKey == "" || c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey) { log.Warnf(log.Global, "%s forex provider no longer supports unset API key requests. Switching to %s FX provider..", c.Currency.ForexProviders[i].Name, DefaultForexProviderExchangeRatesAPI) 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].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].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.Warnf(log.ConfigMgr, "No valid forex providers configured. Defaulting to %s.", DefaultForexProviderExchangeRatesAPI) } } } 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 } err := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType) if err != nil { 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 { err := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType) if err != nil { continue } var pairs []currency.Pair 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() } if c.Logging.AdvancedSettings.ShowLogSystemName == nil { c.Logging.AdvancedSettings.ShowLogSystemName = convert.BoolPtr(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 = convert.BoolPtr(false) } if c.Logging.LoggerFileConfig.MaxSize <= 0 { log.Warnf(log.Global, "Logger rotation size invalid, defaulting to %v", log.DefaultMaxFileSize) c.Logging.LoggerFileConfig.MaxSize = log.DefaultMaxFileSize } log.FileLoggingConfiguredCorrectly = true } log.RWM.Lock() log.GlobalLogConfig = &c.Logging log.RWM.Unlock() logPath := c.GetDataPath("logs") err := common.CreateDir(logPath) if err != nil { return err } log.LogPath = logPath return nil } func (c *Config) checkGCTScriptConfig() error { m.Lock() defer m.Unlock() if c.GCTScript.ScriptTimeout <= 0 { c.GCTScript.ScriptTimeout = gctscript.DefaultTimeoutValue } if c.GCTScript.MaxVirtualMachines == 0 { c.GCTScript.MaxVirtualMachines = gctscript.DefaultMaxVirtualMachines } scriptPath := c.GetDataPath("scripts") err := common.CreateDir(scriptPath) if err != nil { return err } outputPath := filepath.Join(scriptPath, "output") err = common.CreateDir(outputPath) if err != nil { return err } gctscript.ScriptPath = scriptPath return nil } func (c *Config) checkDatabaseConfig() error { m.Lock() defer m.Unlock() if (c.Database == database.Config{}) { c.Database.Driver = database.DBSQLite3 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 == database.DBSQLite || c.Database.Driver == database.DBSQLite3 { databaseDir := c.GetDataPath("database") err := common.CreateDir(databaseDir) if err != nil { return err } database.DB.DataPath = databaseDir } return database.DB.SetConfig(&c.Database) } // 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"} } } // SetNTPCheck allows the user to change how they are prompted for timesync alerts func (c *Config) SetNTPCheck(input io.Reader) (string, error) { m.Lock() defer m.Unlock() reader := bufio.NewReader(input) log.Warnln(log.ConfigMgr, "Your system time is out of sync, this may cause issues with trading") log.Warnln(log.ConfigMgr, "How would you like to show future notifications? (a)lert at startup / (w)arn periodically / (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 } } // DefaultFilePath returns the default config file path // MacOS/Linux: $HOME/.gocryptotrader/config.json or config.dat // Windows: %APPDATA%\GoCryptoTrader\config.json or config.dat // Helpful for printing application usage func DefaultFilePath() string { foundConfig, _, err := GetFilePath("") if err != nil { // If there was no config file, show default location for .json return filepath.Join(common.GetDefaultDataDir(runtime.GOOS), File) } return foundConfig } // GetAndMigrateDefaultPath returns the target config file // migrating it from the old default location to new one, // if it was implicitly loaded from a default location and // wasn't already in the correct 'new' default location func GetAndMigrateDefaultPath(configFile string) (string, error) { filePath, wasDefault, err := GetFilePath(configFile) if err != nil { return "", err } if wasDefault { return migrateConfig(filePath, common.GetDefaultDataDir(runtime.GOOS)) } return filePath, nil } // GetFilePath returns the desired config file or the default config file name // and whether it was loaded from a default location (rather than explicitly specified) func GetFilePath(configFile string) (configPath string, isImplicitDefaultPath bool, err error) { if configFile != "" { return configFile, false, nil } exePath, err := common.GetExecutablePath() if err != nil { return "", false, err } newDir := common.GetDefaultDataDir(runtime.GOOS) defaultPaths := []string{ filepath.Join(exePath, File), filepath.Join(exePath, EncryptedFile), filepath.Join(newDir, File), filepath.Join(newDir, EncryptedFile), } for _, p := range defaultPaths { if file.Exists(p) { configFile = p break } } if configFile == "" { return "", false, fmt.Errorf("config.json file not found in %s, please follow README.md in root dir for config generation", newDir) } return configFile, true, nil } // migrateConfig will move the config file to the target // config directory as `File` or `EncryptedFile` depending on whether the config // is encrypted func migrateConfig(configFile, targetDir string) (string, error) { data, err := ioutil.ReadFile(configFile) if err != nil { return "", err } var target string if ConfirmECS(data) { target = EncryptedFile } else { target = File } target = filepath.Join(targetDir, target) if configFile == target { return configFile, nil } if file.Exists(target) { log.Warnf(log.ConfigMgr, "config file already found in '%s'; not overwriting, defaulting to %s", target, configFile) return configFile, nil } err = file.Move(configFile, target) if err != nil { return "", err } return target, nil } // ReadConfigFromFile reads the configuration from the given file // if target file is encrypted, prompts for encryption key // Also - if not in dryrun mode - it checks if the configuration needs to be encrypted // and stores the file as encrypted, if necessary (prompting for enryption key) func (c *Config) ReadConfigFromFile(configPath string, dryrun bool) error { defaultPath, _, err := GetFilePath(configPath) if err != nil { return err } confFile, err := os.Open(defaultPath) if err != nil { return err } defer confFile.Close() result, wasEncrypted, err := ReadConfig(confFile, func() ([]byte, error) { return PromptForConfigKey(false) }) if err != nil { return fmt.Errorf("error reading config %w", err) } // Override values in the current config *c = *result if dryrun || wasEncrypted || c.EncryptConfig == fileEncryptionDisabled { return nil } if c.EncryptConfig == fileEncryptionPrompt { confirm, err := promptForConfigEncryption() if err != nil { log.Errorf(log.ConfigMgr, "The encryption prompt failed, ignoring for now, next time we will prompt again. Error: %s\n", err) return nil } if confirm { c.EncryptConfig = fileEncryptionEnabled return c.SaveConfigToFile(defaultPath) } c.EncryptConfig = fileEncryptionDisabled err = c.SaveConfigToFile(defaultPath) if err != nil { log.Errorf(log.ConfigMgr, "Cannot save config. Error: %s\n", err) } } return nil } // ReadConfig verifies and checks for encryption and loads the config from a JSON object. // Prompts for decryption key, if target data is encrypted. // Returns the loaded configuration and whether it was encrypted. func ReadConfig(configReader io.Reader, keyProvider func() ([]byte, error)) (*Config, bool, error) { reader := bufio.NewReader(configReader) pref, err := reader.Peek(len(EncryptConfirmString)) if err != nil { return nil, false, err } if !ConfirmECS(pref) { // Read unencrypted configuration decoder := json.NewDecoder(reader) c := &Config{} err = decoder.Decode(c) return c, false, err } conf, err := readEncryptedConfWithKey(reader, keyProvider) return conf, true, err } // readEncryptedConf reads encrypted configuration and requests key from provider func readEncryptedConfWithKey(reader *bufio.Reader, keyProvider func() ([]byte, error)) (*Config, error) { fileData, err := ioutil.ReadAll(reader) if err != nil { return nil, err } for errCounter := 0; errCounter < maxAuthFailures; errCounter++ { key, err := keyProvider() if err != nil { log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err) continue } var c *Config c, err = readEncryptedConf(bytes.NewReader(fileData), key) if err != nil { log.Error(log.ConfigMgr, "Could not decrypt and deserialise data with given key. Invalid password?", err) continue } return c, nil } return nil, errors.New("failed to decrypt config after 3 attempts") } func readEncryptedConf(reader io.Reader, key []byte) (*Config, error) { c := &Config{} data, err := c.decryptConfigData(reader, key) if err != nil { return nil, err } err = json.Unmarshal(data, c) return c, err } // SaveConfigToFile saves your configuration to your desired path as a JSON object. // The function encrypts the data and prompts for encryption key, if necessary func (c *Config) SaveConfigToFile(configPath string) error { defaultPath, _, err := GetFilePath(configPath) if err != nil { return err } var writer *os.File provider := func() (io.Writer, error) { writer, err = file.Writer(defaultPath) return writer, err } defer func() { if writer != nil { err = writer.Close() if err != nil { log.Error(log.Global, err) } } }() return c.Save(provider, func() ([]byte, error) { return PromptForConfigKey(true) }) } // Save saves your configuration to the writer as a JSON object // with encryption, if configured // If there is an error when preparing the data to store, the writer is never requested func (c *Config) Save(writerProvider func() (io.Writer, error), keyProvider func() ([]byte, error)) error { payload, err := json.MarshalIndent(c, "", " ") if err != nil { return err } if c.EncryptConfig == fileEncryptionEnabled { // Ensure we have the key from session or from user if len(c.sessionDK) == 0 { var key []byte key, err = keyProvider() if err != nil { return err } var sessionDK, storedSalt []byte sessionDK, storedSalt, err = makeNewSessionDK(key) if err != nil { return err } c.sessionDK, c.storedSalt = sessionDK, storedSalt } payload, err = c.encryptConfigFile(payload) if err != nil { return err } } configWriter, err := writerProvider() if err != nil { return err } _, err = io.Copy(configWriter, bytes.NewReader(payload)) return err } // 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) } err = c.checkGCTScriptConfig() if err != nil { log.Errorf(log.Global, "Failed to configure gctscript, feature has been disabled: %s\n", err) } c.CheckConnectionMonitorConfig() c.CheckCommunicationsConfig() c.CheckClientBankAccounts() c.CheckBankAccountConfig() 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", defaultHTTPTimeout) c.GlobalHTTPTimeout = defaultHTTPTimeout } 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.ReadConfigFromFile(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 if !dryrun { err = c.SaveConfigToFile(configPath) if err != nil { return err } } return c.LoadConfig(configPath, dryrun) } // GetConfig returns a pointer to a configuration object func GetConfig() *Config { return &Cfg } // RemoveExchange removes an exchange config func (c *Config) RemoveExchange(exchName string) bool { m.Lock() defer m.Unlock() for x := range c.Exchanges { if strings.EqualFold(c.Exchanges[x].Name, exchName) { c.Exchanges = append(c.Exchanges[:x], c.Exchanges[x+1:]...) return true } } return false } // AssetTypeEnabled checks to see if the asset type is enabled in configuration func (c *Config) AssetTypeEnabled(a asset.Item, exch string) (bool, error) { cfg, err := c.GetExchangeConfig(exch) if err != nil { return false, err } err = cfg.CurrencyPairs.IsAssetEnabled(a) if err != nil { return false, nil } return true, nil } // GetDataPath gets the data path for the given subpath func (c *Config) GetDataPath(elem ...string) string { var baseDir string if c.DataDirectory != "" { baseDir = c.DataDirectory } else { baseDir = common.GetDefaultDataDir(runtime.GOOS) } return filepath.Join(append([]string{baseDir}, elem...)...) }