mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
Config: Add versioning (#1671)
* Config: Version Management * Engine: Improve visibility of TestConfigAllJsonResponse failures * Config: Update cmd/config to allow upgrades * Config: Add Version2 to rename GDAX * Config: Restructure versioning to share types This restructure allows us to share types between versions, avoids needing to import the versions, and puts the test fixtures in same package. It's a win on all fronts * Config: Fix SetNTPCheck using log Called from engine before logger is inited, and also just wrong to use log to communicate with user * Config: Improve TestMigrateConfig * Config: Drop requirement for versions to be registered in sequence Checking the versions at Deploy is much saner. * Config: Fix file encrypted but flag not set * Config: Add -edit and encryption upgrade to cmd/config This simplifies the handling for encryption prompts by moving it to a field on config, allowing us to simplify all the places were were passing around config Also moves password entry to being secure (echo-off) * Tests: Fix inconsistent should/must assertions
This commit is contained in:
@@ -1,82 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"log"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/file"
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
)
|
||||
|
||||
// EncryptOrDecrypt returns a string from a boolean
|
||||
func EncryptOrDecrypt(encrypt bool) string {
|
||||
if encrypt {
|
||||
return "encrypted"
|
||||
}
|
||||
return "decrypted"
|
||||
}
|
||||
var commands = []string{"upgrade", "encrypt", "decrypt"}
|
||||
|
||||
func main() {
|
||||
var inFile, outFile, key string
|
||||
var encrypt bool
|
||||
fmt.Println("GoCryptoTrader: config-helper tool")
|
||||
|
||||
defaultCfgFile := config.DefaultFilePath()
|
||||
flag.StringVar(&inFile, "infile", defaultCfgFile, "The config input file to process.")
|
||||
flag.StringVar(&outFile, "outfile", defaultCfgFile+".out", "The config output file.")
|
||||
flag.BoolVar(&encrypt, "encrypt", true, "Whether to encrypt or decrypt.")
|
||||
flag.StringVar(&key, "key", "", "The key to use for AES encryption.")
|
||||
flag.Parse()
|
||||
|
||||
log.Println("GoCryptoTrader: config-helper tool.")
|
||||
var in, out, keyStr string
|
||||
var inplace bool
|
||||
|
||||
if key == "" {
|
||||
result, err := config.PromptForConfigKey(false)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to obtain encryption/decryption key: %s", err)
|
||||
}
|
||||
key = string(result)
|
||||
fs := flag.NewFlagSet("config", flag.ExitOnError)
|
||||
fs.Usage = func() { usage(fs) }
|
||||
fs.StringVar(&in, "in", defaultCfgFile, "The config input file to process")
|
||||
fs.StringVar(&out, "out", "[in].out", "The config output file")
|
||||
fs.BoolVar(&inplace, "edit", false, "Edit; Save result to the original file")
|
||||
fs.StringVar(&keyStr, "key", "", "The key to use for AES encryption")
|
||||
|
||||
cmd, args := parseCommand(os.Args[1:])
|
||||
if cmd == "" {
|
||||
usage(fs)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
||||
if inplace {
|
||||
out = in
|
||||
} else if out == "[in].out" {
|
||||
out = in + ".out"
|
||||
}
|
||||
|
||||
key := []byte(keyStr)
|
||||
var err error
|
||||
switch cmd {
|
||||
case "upgrade":
|
||||
err = upgradeFile(in, out, key)
|
||||
case "decrypt":
|
||||
err = encryptWrapper(in, out, key, false, decryptFile)
|
||||
case "encrypt":
|
||||
err = encryptWrapper(in, out, key, true, encryptFile)
|
||||
}
|
||||
|
||||
fileData, err := os.ReadFile(inFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read input file %s. Error: %s.", inFile, err)
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
||||
if config.ConfirmECS(fileData) && encrypt {
|
||||
log.Println("File is already encrypted. Decrypting..")
|
||||
encrypt = false
|
||||
}
|
||||
|
||||
if !config.ConfirmECS(fileData) && !encrypt {
|
||||
var result interface{}
|
||||
errf := json.Unmarshal(fileData, &result)
|
||||
if errf != nil {
|
||||
log.Fatal(errf)
|
||||
}
|
||||
log.Println("File is already decrypted. Encrypting..")
|
||||
encrypt = true
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if encrypt {
|
||||
data, err = config.EncryptConfigFile(fileData, []byte(key))
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to encrypt config data. Error: %s.", err)
|
||||
}
|
||||
} else {
|
||||
data, err = config.DecryptConfigFile(fileData, []byte(key))
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to decrypt config data. Error: %s.", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = file.Write(outFile, data)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to write output file %s. Error: %s", outFile, err)
|
||||
}
|
||||
log.Printf(
|
||||
"Successfully %s input file %s and wrote output to %s.\n",
|
||||
EncryptOrDecrypt(encrypt), inFile, outFile,
|
||||
)
|
||||
fmt.Println("Success! File written to " + out)
|
||||
}
|
||||
|
||||
func upgradeFile(in, out string, key []byte) error {
|
||||
c := &config.Config{
|
||||
EncryptionKeyProvider: func(_ bool) ([]byte, error) {
|
||||
if len(key) != 0 {
|
||||
return key, nil
|
||||
}
|
||||
return config.PromptForConfigKey(false)
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.ReadConfigFromFile(in, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SaveConfigToFile(out)
|
||||
}
|
||||
|
||||
type encryptFunc func(string, []byte) ([]byte, error)
|
||||
|
||||
func encryptWrapper(in, out string, key []byte, confirmKey bool, fn encryptFunc) error {
|
||||
if len(key) == 0 {
|
||||
var err error
|
||||
if key, err = config.PromptForConfigKey(confirmKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
outData, err := fn(in, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Write(out, outData); err != nil {
|
||||
return fmt.Errorf("unable to write output file %s; Error: %w", out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encryptFile(in string, key []byte) ([]byte, error) {
|
||||
if config.IsFileEncrypted(in) {
|
||||
return nil, errors.New("file is already encrypted")
|
||||
}
|
||||
outData, err := config.EncryptConfigFile(readFile(in), key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to encrypt config data. Error: %w", err)
|
||||
}
|
||||
return outData, nil
|
||||
}
|
||||
|
||||
func decryptFile(in string, key []byte) ([]byte, error) {
|
||||
if !config.IsFileEncrypted(in) {
|
||||
return nil, errors.New("file is already decrypted")
|
||||
}
|
||||
outData, err := config.DecryptConfigFile(readFile(in), key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt config data. Error: %w", err)
|
||||
}
|
||||
if outData, err = jsonparser.Set(outData, []byte("-1"), "encryptConfig"); err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt config data. Error: %w", err)
|
||||
}
|
||||
return outData, nil
|
||||
}
|
||||
|
||||
func readFile(in string) []byte {
|
||||
fileData, err := os.ReadFile(in)
|
||||
if err != nil {
|
||||
fatal("Unable to read input file " + in + "; Error: " + err.Error())
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
func fatal(msg string) {
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// parseCommand will return the single non-flag parameter from os.Args, and return the remaining args
|
||||
// If none is provided, too many, usage() will be called and exit 1
|
||||
func parseCommand(a []string) (cmd string, args []string) {
|
||||
cmds, rem := []string{}, []string{}
|
||||
for _, s := range a {
|
||||
if slices.Contains(commands, s) {
|
||||
cmds = append(cmds, s)
|
||||
} else {
|
||||
rem = append(rem, s)
|
||||
}
|
||||
}
|
||||
switch len(cmds) {
|
||||
case 0:
|
||||
fmt.Fprintln(os.Stderr, "No command provided")
|
||||
case 1: //
|
||||
return cmds[0], rem
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "Too many commands provided: "+strings.Join(cmds, ", "))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// usage prints command usage and exits 1
|
||||
func usage(fs *flag.FlagSet) {
|
||||
//nolint:dupword // deliberate duplication of commands
|
||||
fmt.Fprintln(os.Stderr, `
|
||||
Usage:
|
||||
config [arguments] <command>
|
||||
|
||||
The commands are:
|
||||
encrypt encrypt infile and write to outfile
|
||||
decrypt decrypt infile and write to outfile
|
||||
upgrade upgrade the version of a decrypted config file
|
||||
|
||||
The arguments are:`)
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncryptOrDecrypt(t *testing.T) {
|
||||
reValue := EncryptOrDecrypt(true)
|
||||
if reValue != "encrypted" {
|
||||
t.Error(
|
||||
"expected encrypted",
|
||||
)
|
||||
}
|
||||
reValue = EncryptOrDecrypt(false)
|
||||
if reValue != "decrypted" {
|
||||
t.Error(
|
||||
"expected decrypted",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,8 @@ var (
|
||||
ErrNoResponse = errors.New("no response")
|
||||
ErrTypeAssertFailure = errors.New("type assert failure")
|
||||
ErrUnknownError = errors.New("unknown error")
|
||||
ErrGettingField = errors.New("error getting field")
|
||||
ErrSettingField = errors.New("error setting field")
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
612
config/config.go
612
config/config.go
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"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/config/versions"
|
||||
"github.com/thrasher-corp/gocryptotrader/connchecker"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider"
|
||||
@@ -30,9 +32,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// errExchangeConfigIsNil defines an error when the config is nil
|
||||
errExchangeConfigIsNil = errors.New("exchange config is nil")
|
||||
errPairsManagerIsNil = errors.New("currency pairs manager is nil")
|
||||
errDecryptFailed = errors.New("failed to decrypt config after 3 attempts")
|
||||
)
|
||||
|
||||
// GetCurrencyConfig returns currency configurations
|
||||
@@ -40,28 +42,22 @@ func (c *Config) GetCurrencyConfig() currency.Config {
|
||||
return c.Currency
|
||||
}
|
||||
|
||||
// GetExchangeBankAccounts returns banking details associated with an exchange
|
||||
// for depositing funds
|
||||
// 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()
|
||||
e, err := c.GetExchangeConfig(exchangeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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.StringSliceCompareInsensitive(
|
||||
strings.Split(c.Exchanges[x].BankAccounts[y].SupportedCurrencies, ","),
|
||||
depositingCurrency) {
|
||||
return &c.Exchanges[x].BankAccounts[y], nil
|
||||
}
|
||||
}
|
||||
for y := range e.BankAccounts {
|
||||
if strings.EqualFold(e.BankAccounts[y].ID, id) {
|
||||
if common.StringSliceCompareInsensitive(strings.Split(e.BankAccounts[y].SupportedCurrencies, ","), depositingCurrency) {
|
||||
return &e.BankAccounts[y], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("exchange %s bank details not found for %s",
|
||||
exchangeName,
|
||||
depositingCurrency)
|
||||
|
||||
return nil, fmt.Errorf("exchange %s bank details not found for %s", exchangeName, depositingCurrency)
|
||||
}
|
||||
|
||||
// UpdateExchangeBankAccounts updates the configuration for the associated
|
||||
@@ -840,263 +836,207 @@ func (c *Config) UpdateExchangeConfig(e *Exchange) error {
|
||||
// exchanges
|
||||
func (c *Config) CheckExchangeConfigValues() error {
|
||||
if len(c.Exchanges) == 0 {
|
||||
return errors.New("no exchange configs found")
|
||||
return errNoEnabledExchanges
|
||||
}
|
||||
|
||||
exchanges := 0
|
||||
for i := range c.Exchanges {
|
||||
if strings.EqualFold(c.Exchanges[i].Name, "GDAX") {
|
||||
c.Exchanges[i].Name = "CoinbasePro"
|
||||
}
|
||||
e := &c.Exchanges[i]
|
||||
|
||||
// Check to see if the old API storage format is used
|
||||
if c.Exchanges[i].APIKey != nil {
|
||||
if e.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
|
||||
e.API.AuthenticatedSupport = *e.AuthenticatedAPISupport
|
||||
if e.AuthenticatedWebsocketAPISupport != nil {
|
||||
e.API.AuthenticatedWebsocketSupport = *e.AuthenticatedWebsocketAPISupport
|
||||
}
|
||||
c.Exchanges[i].API.Credentials.Key = *c.Exchanges[i].APIKey
|
||||
c.Exchanges[i].API.Credentials.Secret = *c.Exchanges[i].APISecret
|
||||
e.API.Credentials.Key = *e.APIKey
|
||||
e.API.Credentials.Secret = *e.APISecret
|
||||
|
||||
if c.Exchanges[i].APIAuthPEMKey != nil {
|
||||
c.Exchanges[i].API.Credentials.PEMKey = *c.Exchanges[i].APIAuthPEMKey
|
||||
if e.APIAuthPEMKey != nil {
|
||||
e.API.Credentials.PEMKey = *e.APIAuthPEMKey
|
||||
}
|
||||
|
||||
if c.Exchanges[i].APIAuthPEMKeySupport != nil {
|
||||
c.Exchanges[i].API.PEMKeySupport = *c.Exchanges[i].APIAuthPEMKeySupport
|
||||
if e.APIAuthPEMKeySupport != nil {
|
||||
e.API.PEMKeySupport = *e.APIAuthPEMKeySupport
|
||||
}
|
||||
|
||||
if c.Exchanges[i].ClientID != nil {
|
||||
c.Exchanges[i].API.Credentials.ClientID = *c.Exchanges[i].ClientID
|
||||
if e.ClientID != nil {
|
||||
e.API.Credentials.ClientID = *e.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
|
||||
e.AuthenticatedAPISupport = nil
|
||||
e.AuthenticatedWebsocketAPISupport = nil
|
||||
e.APIKey = nil
|
||||
e.APISecret = nil
|
||||
e.ClientID = nil
|
||||
e.APIAuthPEMKeySupport = nil
|
||||
e.APIAuthPEMKey = nil
|
||||
e.APIURL = nil
|
||||
e.APIURLSecondary = nil
|
||||
e.WebsocketURL = nil
|
||||
}
|
||||
|
||||
if c.Exchanges[i].Features == nil {
|
||||
c.Exchanges[i].Features = &FeaturesConfig{}
|
||||
if e.Features == nil {
|
||||
e.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 e.SupportsAutoPairUpdates != nil {
|
||||
e.Features.Supports.RESTCapabilities.AutoPairUpdates = *e.SupportsAutoPairUpdates
|
||||
e.Features.Enabled.AutoPairUpdates = *e.SupportsAutoPairUpdates
|
||||
e.SupportsAutoPairUpdates = nil
|
||||
}
|
||||
|
||||
if c.Exchanges[i].Websocket != nil {
|
||||
c.Exchanges[i].Features.Enabled.Websocket = *c.Exchanges[i].Websocket
|
||||
c.Exchanges[i].Websocket = nil
|
||||
if e.Websocket != nil {
|
||||
e.Features.Enabled.Websocket = *e.Websocket
|
||||
e.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 err := e.CurrencyPairs.SetDelimitersFromConfig(); err != nil {
|
||||
return fmt.Errorf("%s: %w", e.Name, err)
|
||||
}
|
||||
|
||||
if c.Exchanges[i].PairsLastUpdated != nil {
|
||||
c.Exchanges[i].CurrencyPairs.LastUpdated = *c.Exchanges[i].PairsLastUpdated
|
||||
}
|
||||
assets := e.CurrencyPairs.GetAssetTypes(false)
|
||||
if len(assets) == 0 {
|
||||
e.Enabled = false
|
||||
log.Warnf(log.ConfigMgr, "%s no assets found, disabling...", e.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
err := c.Exchanges[i].CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{
|
||||
AssetEnabled: convert.BoolPtr(true),
|
||||
Available: availPairs,
|
||||
Enabled: enabledPairs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := c.Exchanges[i].CurrencyPairs.SetDelimitersFromConfig(); err != nil {
|
||||
return fmt.Errorf("%s: %w", c.Exchanges[i].Name, err)
|
||||
}
|
||||
|
||||
assets := c.Exchanges[i].CurrencyPairs.GetAssetTypes(false)
|
||||
if len(assets) == 0 {
|
||||
c.Exchanges[i].Enabled = false
|
||||
log.Warnf(log.ConfigMgr, "%s no assets found, disabling...", c.Exchanges[i].Name)
|
||||
continue
|
||||
}
|
||||
|
||||
var atLeastOne bool
|
||||
for index := range assets {
|
||||
err := c.Exchanges[i].CurrencyPairs.IsAssetEnabled(assets[index])
|
||||
if err != nil {
|
||||
if errors.Is(err, currency.ErrAssetIsNil) {
|
||||
// Checks if we have an old config without the ability to
|
||||
// enable disable the entire asset
|
||||
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 {
|
||||
// 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 {
|
||||
for _, a := range assets {
|
||||
if err := e.CurrencyPairs.IsAssetEnabled(a); errors.Is(err, currency.ErrAssetIsNil) {
|
||||
// Checks if we have an old config without the ability to enable disable the entire asset
|
||||
log.Warnf(log.ConfigMgr, "Exchange %s: upgrading config for asset type %s and setting enabled.\n", e.Name, a)
|
||||
if err := e.CurrencyPairs.SetAssetEnabled(a, true); 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 enabled := e.CurrencyPairs.GetAssetTypes(true); len(enabled) == 0 {
|
||||
// turn on an asset if all disabled
|
||||
log.Warnf(log.ConfigMgr, "%s assets disabled, turning on asset %s", e.Name, assets[0])
|
||||
if err := e.CurrencyPairs.SetAssetEnabled(assets[0], true); err != nil {
|
||||
return err
|
||||
}
|
||||
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].Orderbook.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].Orderbook.WebsocketBufferLimit = defaultWebsocketOrderbookBufferLimit
|
||||
}
|
||||
if c.Exchanges[i].Orderbook.PublishPeriod == nil || c.Exchanges[i].Orderbook.PublishPeriod.Nanoseconds() < 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s Websocket orderbook publish period value not set, defaulting to %v.",
|
||||
c.Exchanges[i].Name,
|
||||
DefaultOrderbookPublishPeriod)
|
||||
publishPeriod := DefaultOrderbookPublishPeriod
|
||||
c.Exchanges[i].Orderbook.PublishPeriod = &publishPeriod
|
||||
}
|
||||
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 !e.Enabled {
|
||||
continue
|
||||
}
|
||||
if e.Name == "" {
|
||||
log.Errorf(log.ConfigMgr, "%s: #%d", errExchangeNameEmpty, i)
|
||||
e.Enabled = false
|
||||
continue
|
||||
}
|
||||
if (e.API.AuthenticatedSupport || e.API.AuthenticatedWebsocketSupport) &&
|
||||
e.API.CredentialsValidator != nil {
|
||||
var failed bool
|
||||
if e.API.CredentialsValidator.RequiresKey &&
|
||||
(e.API.Credentials.Key == "" || e.API.Credentials.Key == DefaultAPIKey) {
|
||||
failed = true
|
||||
}
|
||||
|
||||
if e.API.CredentialsValidator.RequiresSecret &&
|
||||
(e.API.Credentials.Secret == "" || e.API.Credentials.Secret == DefaultAPISecret) {
|
||||
failed = true
|
||||
}
|
||||
|
||||
if e.API.CredentialsValidator.RequiresClientID &&
|
||||
(e.API.Credentials.ClientID == DefaultAPIClientID || e.API.Credentials.ClientID == "") {
|
||||
failed = true
|
||||
}
|
||||
|
||||
if failed {
|
||||
e.API.AuthenticatedSupport = false
|
||||
e.API.AuthenticatedWebsocketSupport = false
|
||||
log.Warnf(log.ConfigMgr, warningExchangeAuthAPIDefaultOrEmptyValues, e.Name)
|
||||
}
|
||||
}
|
||||
if !e.Features.Supports.RESTCapabilities.AutoPairUpdates &&
|
||||
!e.Features.Supports.WebsocketCapabilities.AutoPairUpdates {
|
||||
lastUpdated := convert.UnixTimestampToTime(e.CurrencyPairs.LastUpdated)
|
||||
lastUpdated = lastUpdated.AddDate(0, 0, pairsLastUpdatedWarningThreshold)
|
||||
if lastUpdated.Unix() <= time.Now().Unix() {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
warningPairsLastUpdatedThresholdExceeded,
|
||||
e.Name,
|
||||
pairsLastUpdatedWarningThreshold)
|
||||
}
|
||||
}
|
||||
if e.HTTPTimeout <= 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s HTTP Timeout value not set, defaulting to %v.\n",
|
||||
e.Name,
|
||||
defaultHTTPTimeout)
|
||||
e.HTTPTimeout = defaultHTTPTimeout
|
||||
}
|
||||
|
||||
if e.WebsocketResponseCheckTimeout <= 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s Websocket response check timeout value not set, defaulting to %v.",
|
||||
e.Name,
|
||||
DefaultWebsocketResponseCheckTimeout)
|
||||
e.WebsocketResponseCheckTimeout = DefaultWebsocketResponseCheckTimeout
|
||||
}
|
||||
|
||||
if e.WebsocketResponseMaxLimit <= 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s Websocket response max limit value not set, defaulting to %v.",
|
||||
e.Name,
|
||||
DefaultWebsocketResponseMaxLimit)
|
||||
e.WebsocketResponseMaxLimit = DefaultWebsocketResponseMaxLimit
|
||||
}
|
||||
if e.WebsocketTrafficTimeout <= 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s Websocket response traffic timeout value not set, defaulting to %v.",
|
||||
e.Name,
|
||||
DefaultWebsocketTrafficTimeout)
|
||||
e.WebsocketTrafficTimeout = DefaultWebsocketTrafficTimeout
|
||||
}
|
||||
if e.Orderbook.WebsocketBufferLimit <= 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.",
|
||||
e.Name,
|
||||
defaultWebsocketOrderbookBufferLimit)
|
||||
e.Orderbook.WebsocketBufferLimit = defaultWebsocketOrderbookBufferLimit
|
||||
}
|
||||
if e.Orderbook.PublishPeriod == nil || e.Orderbook.PublishPeriod.Nanoseconds() < 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Exchange %s Websocket orderbook publish period value not set, defaulting to %v.",
|
||||
e.Name,
|
||||
DefaultOrderbookPublishPeriod)
|
||||
publishPeriod := DefaultOrderbookPublishPeriod
|
||||
e.Orderbook.PublishPeriod = &publishPeriod
|
||||
}
|
||||
err := c.CheckPairConsistency(e.Name)
|
||||
if err != nil {
|
||||
log.Errorf(log.ConfigMgr,
|
||||
"Exchange %s: CheckPairConsistency error: %s\n",
|
||||
e.Name,
|
||||
err)
|
||||
e.Enabled = false
|
||||
continue
|
||||
}
|
||||
for x := range e.BankAccounts {
|
||||
if !e.BankAccounts[x].Enabled {
|
||||
continue
|
||||
}
|
||||
err := e.BankAccounts[x].Validate()
|
||||
if err != nil {
|
||||
e.BankAccounts[x].Enabled = false
|
||||
log.Warnln(log.ConfigMgr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
exchanges++
|
||||
}
|
||||
|
||||
if exchanges == 0 {
|
||||
return errors.New(ErrNoEnabledExchanges)
|
||||
return errNoEnabledExchanges
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1350,8 +1290,8 @@ func (c *Config) SetNTPCheck(input io.Reader) (string, error) {
|
||||
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")
|
||||
fmt.Println("Your system time is out of sync, this may cause issues with trading")
|
||||
fmt.Println("How would you like to show future notifications? (a)lert at startup / (w)arn periodically / (d)isable")
|
||||
|
||||
var resp string
|
||||
answered := false
|
||||
@@ -1376,8 +1316,7 @@ func (c *Config) SetNTPCheck(input io.Reader) (string, error) {
|
||||
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")
|
||||
fmt.Println("Invalid option selected, please try again (a)lert / (w)arn / (d)isable")
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
@@ -1510,13 +1449,8 @@ func GetFilePath(configFile string) (configPath string, isImplicitDefaultPath bo
|
||||
// config directory as `File` or `EncryptedFile` depending on whether the config
|
||||
// is encrypted
|
||||
func migrateConfig(configFile, targetDir string) (string, error) {
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var target string
|
||||
if ConfirmECS(data) {
|
||||
if IsFileEncrypted(configFile) {
|
||||
target = EncryptedFile
|
||||
} else {
|
||||
target = File
|
||||
@@ -1530,115 +1464,96 @@ func migrateConfig(configFile, targetDir string) (string, error) {
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
err = file.Move(configFile, target)
|
||||
if err != nil {
|
||||
if err := file.Move(configFile, target); 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 encryption key)
|
||||
func (c *Config) ReadConfigFromFile(configPath string, dryrun bool) error {
|
||||
defaultPath, _, err := GetFilePath(configPath)
|
||||
// ReadConfigFromFile loads Config from the path
|
||||
// If encrypted, prompts for encryption key
|
||||
// Unless dryrun checks if the configuration needs to be encrypted and resaves, prompting for key
|
||||
func (c *Config) ReadConfigFromFile(path string, dryrun bool) error {
|
||||
var err error
|
||||
path, _, err = GetFilePath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confFile, err := os.Open(defaultPath)
|
||||
f, err := os.Open(path)
|
||||
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
|
||||
defer f.Close()
|
||||
|
||||
if dryrun || wasEncrypted || c.EncryptConfig == fileEncryptionDisabled {
|
||||
if err := c.readConfig(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dryrun || c.EncryptConfig != fileEncryptionPrompt || IsFileEncrypted(path) {
|
||||
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
|
||||
return c.saveWithEncryptPrompt(path)
|
||||
}
|
||||
|
||||
// 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))
|
||||
// readConfig loads config from a io.Reader into the config object
|
||||
// versions manager will upgrade/downgrade if appropriate
|
||||
// If encrypted, prompts for encryption key
|
||||
func (c *Config) readConfig(d io.Reader) error {
|
||||
j, err := io.ReadAll(d)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return err
|
||||
}
|
||||
|
||||
if !ConfirmECS(pref) {
|
||||
// Read unencrypted configuration
|
||||
decoder := json.NewDecoder(reader)
|
||||
c := &Config{}
|
||||
err = decoder.Decode(c)
|
||||
return c, false, err
|
||||
if IsEncrypted(j) {
|
||||
if j, err = c.decryptConfig(j); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
conf, err := readEncryptedConfWithKey(reader, keyProvider)
|
||||
return conf, true, err
|
||||
if j, err = versions.Manager.Deploy(context.Background(), j); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(j, c)
|
||||
}
|
||||
|
||||
// readEncryptedConf reads encrypted configuration and requests key from provider
|
||||
func readEncryptedConfWithKey(reader *bufio.Reader, keyProvider func() ([]byte, error)) (*Config, error) {
|
||||
fileData, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// saveWithEncryptPrompt will prompt the user if they want to encrypt their config
|
||||
// If they agree, c.EncryptConfig is set to Enabled, the config is encrypted and saved
|
||||
// Otherwise, c.EncryptConfig is set to Disabled and the file is resaved
|
||||
func (c *Config) saveWithEncryptPrompt(path string) error {
|
||||
if confirm, err := promptForConfigEncryption(); err != nil {
|
||||
return nil //nolint:nilerr // Ignore encryption prompt failures; The user will be prompted again
|
||||
} else if confirm {
|
||||
c.EncryptConfig = fileEncryptionEnabled
|
||||
return c.SaveConfigToFile(path)
|
||||
}
|
||||
|
||||
c.EncryptConfig = fileEncryptionDisabled
|
||||
return c.SaveConfigToFile(path)
|
||||
}
|
||||
|
||||
// decryptConfig reads encrypted configuration and requests key from provider
|
||||
func (c *Config) decryptConfig(j []byte) ([]byte, error) {
|
||||
for range maxAuthFailures {
|
||||
key, err := keyProvider()
|
||||
f := c.EncryptionKeyProvider
|
||||
if f == nil {
|
||||
f = PromptForConfigKey
|
||||
}
|
||||
key, err := f(false)
|
||||
if err != nil {
|
||||
log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var c *Config
|
||||
c, err = readEncryptedConf(bytes.NewReader(fileData), key)
|
||||
d, err := c.decryptConfigData(j, key)
|
||||
if err != nil {
|
||||
log.Errorln(log.ConfigMgr, "Could not decrypt and deserialise data with given key. Invalid password?", err)
|
||||
continue
|
||||
}
|
||||
return c, nil
|
||||
return d, 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
|
||||
return nil, errDecryptFailed
|
||||
}
|
||||
|
||||
// SaveConfigToFile saves your configuration to your desired path as a JSON object.
|
||||
@@ -1661,13 +1576,12 @@ func (c *Config) SaveConfigToFile(configPath string) error {
|
||||
}
|
||||
}
|
||||
}()
|
||||
return c.Save(provider, func() ([]byte, error) { return PromptForConfigKey(true) })
|
||||
return c.Save(provider)
|
||||
}
|
||||
|
||||
// Save saves your configuration to the writer as a JSON object
|
||||
// with encryption, if configured
|
||||
// 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 {
|
||||
func (c *Config) Save(writerProvider func() (io.Writer, error)) error {
|
||||
payload, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1676,14 +1590,15 @@ func (c *Config) Save(writerProvider func() (io.Writer, error), keyProvider func
|
||||
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 {
|
||||
f := c.EncryptionKeyProvider
|
||||
if f == nil {
|
||||
f = PromptForConfigKey
|
||||
}
|
||||
var key, sessionDK, storedSalt []byte
|
||||
if key, err = f(true); err != nil {
|
||||
return err
|
||||
}
|
||||
var sessionDK, storedSalt []byte
|
||||
sessionDK, storedSalt, err = makeNewSessionDK(key)
|
||||
if err != nil {
|
||||
if sessionDK, storedSalt, err = makeNewSessionDK(key); err != nil {
|
||||
return err
|
||||
}
|
||||
c.sessionDK, c.storedSalt = sessionDK, storedSalt
|
||||
@@ -1746,30 +1661,20 @@ func (c *Config) CheckRemoteControlConfig() {
|
||||
|
||||
// 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)
|
||||
if err := c.CheckLoggerConfig(); 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)
|
||||
if err := c.checkDatabaseConfig(); err != nil {
|
||||
log.Errorf(log.DatabaseMgr, "Failed to configure database: %v", err)
|
||||
}
|
||||
|
||||
err = c.CheckExchangeConfigValues()
|
||||
if err != nil {
|
||||
return fmt.Errorf(ErrCheckingConfigValues, err)
|
||||
if err := c.CheckExchangeConfigValues(); err != nil {
|
||||
return fmt.Errorf("%w: %w", errCheckingConfigValues, err)
|
||||
}
|
||||
|
||||
err = c.checkGCTScriptConfig()
|
||||
if err != nil {
|
||||
log.Errorf(log.ConfigMgr,
|
||||
"Failed to configure gctscript, feature has been disabled: %s\n",
|
||||
err)
|
||||
if err := c.checkGCTScriptConfig(); err != nil {
|
||||
log.Errorf(log.ConfigMgr, "Failed to configure gctscript, feature has been disabled: %s\n", err)
|
||||
}
|
||||
|
||||
c.CheckConnectionMonitorConfig()
|
||||
@@ -1782,15 +1687,12 @@ func (c *Config) CheckConfig() error {
|
||||
c.CheckRemoteControlConfig()
|
||||
c.CheckSyncManagerConfig()
|
||||
|
||||
err = c.CheckCurrencyConfigValues()
|
||||
if err != nil {
|
||||
if err := c.CheckCurrencyConfigValues(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.GlobalHTTPTimeout <= 0 {
|
||||
log.Warnf(log.ConfigMgr,
|
||||
"Global HTTP Timeout value not set, defaulting to %v.\n",
|
||||
defaultHTTPTimeout)
|
||||
log.Warnf(log.ConfigMgr, "Global HTTP Timeout value not set, defaulting to %v.\n", defaultHTTPTimeout)
|
||||
c.GlobalHTTPTimeout = defaultHTTPTimeout
|
||||
}
|
||||
|
||||
@@ -1805,7 +1707,7 @@ func (c *Config) CheckConfig() error {
|
||||
func (c *Config) LoadConfig(configPath string, dryrun bool) error {
|
||||
err := c.ReadConfigFromFile(configPath, dryrun)
|
||||
if err != nil {
|
||||
return fmt.Errorf(ErrFailureOpeningConfig, configPath, err)
|
||||
return fmt.Errorf("%w (%s): %w", ErrFailureOpeningConfig, configPath, err)
|
||||
}
|
||||
return c.CheckConfig()
|
||||
}
|
||||
|
||||
@@ -8,29 +8,39 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
// EncryptConfirmString has a the general confirmation string to allow us to
|
||||
// see if the file is correctly encrypted
|
||||
EncryptConfirmString = "THORS-HAMMER"
|
||||
// SaltPrefix string
|
||||
SaltPrefix = "~GCT~SO~SALTY~"
|
||||
// SaltRandomLength is the number of random bytes to append after the prefix string
|
||||
SaltRandomLength = 12
|
||||
saltRandomLength = 12
|
||||
)
|
||||
|
||||
errAESBlockSize = "config file data is too small for the AES required block size"
|
||||
// Public errors
|
||||
var (
|
||||
ErrSettingEncryptConfig = errors.New("error setting EncryptConfig during encrypt config file")
|
||||
)
|
||||
|
||||
var (
|
||||
errAESBlockSize = errors.New("config file data is too small for the AES required block size")
|
||||
errNoPrefix = errors.New("data does not start with Encryption Prefix")
|
||||
errKeyIsEmpty = errors.New("key is empty")
|
||||
errUserInput = errors.New("error getting user input")
|
||||
|
||||
// encryptionPrefix is a prefix to tell us the file is encrypted
|
||||
encryptionPrefix = []byte("THORS-HAMMER")
|
||||
saltPrefix = []byte("~GCT~SO~SALTY~")
|
||||
)
|
||||
|
||||
// promptForConfigEncryption asks for encryption confirmation
|
||||
// returns true if encryption was desired, false otherwise
|
||||
func promptForConfigEncryption() (bool, error) {
|
||||
log.Println("Would you like to encrypt your config file (y/n)?")
|
||||
fmt.Println("Would you like to encrypt your config file (y/n)?")
|
||||
|
||||
input := ""
|
||||
if _, err := fmt.Scanln(&input); err != nil {
|
||||
@@ -40,52 +50,52 @@ func promptForConfigEncryption() (bool, error) {
|
||||
return common.YesOrNo(input), nil
|
||||
}
|
||||
|
||||
// Unencrypted provides the default key provider implementation for unencrypted files
|
||||
func Unencrypted() ([]byte, error) {
|
||||
return nil, errors.New("encryption key was requested, no key provided")
|
||||
}
|
||||
|
||||
// PromptForConfigKey asks for configuration key
|
||||
// if initialSetup is true, the password needs to be repeated
|
||||
func PromptForConfigKey(initialSetup bool) ([]byte, error) {
|
||||
var cryptoKey []byte
|
||||
|
||||
for {
|
||||
log.Println("Please enter in your password: ")
|
||||
pwPrompt := func(i *[]byte) error {
|
||||
_, err := fmt.Scanln(i)
|
||||
return err
|
||||
}
|
||||
|
||||
var p1 []byte
|
||||
err := pwPrompt(&p1)
|
||||
func PromptForConfigKey(confirmKey bool) ([]byte, error) {
|
||||
for range 3 {
|
||||
key, err := getSensitiveInput("Please enter encryption key: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %w", errUserInput, err)
|
||||
}
|
||||
|
||||
if !initialSetup {
|
||||
cryptoKey = p1
|
||||
break
|
||||
if len(key) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p2 []byte
|
||||
log.Println("Please re-enter your password: ")
|
||||
err = pwPrompt(&p2)
|
||||
if !confirmKey {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
conf, err := getSensitiveInput("Please re-enter key: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %w", errUserInput, err)
|
||||
}
|
||||
|
||||
if bytes.Equal(p1, p2) {
|
||||
cryptoKey = p1
|
||||
break
|
||||
if bytes.Equal(key, conf) {
|
||||
return key, nil
|
||||
}
|
||||
log.Println("Passwords did not match, please try again.")
|
||||
fmt.Println("Keys did not match, please try again.")
|
||||
}
|
||||
return cryptoKey, nil
|
||||
return nil, fmt.Errorf("%w: %w", errUserInput, io.EOF)
|
||||
}
|
||||
|
||||
// EncryptConfigFile encrypts configuration data that is parsed in with a key
|
||||
// and returns it as a byte array with an error
|
||||
// getSensitiveInput reads input from stdin, with echo off if stdin is a terminal
|
||||
func getSensitiveInput(prompt string) (resp []byte, err error) {
|
||||
fmt.Print(prompt)
|
||||
defer fmt.Println()
|
||||
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return term.ReadPassword(int(os.Stdin.Fd()))
|
||||
}
|
||||
// Can't use bufio.* because it consumes the whole input in one go, even with s.Buffer(1)
|
||||
for buf := make([]byte, 1); err == nil && buf[0] != '\n'; {
|
||||
if _, err = os.Stdin.Read(buf); err == nil {
|
||||
resp = append(resp, buf[0])
|
||||
}
|
||||
}
|
||||
return bytes.TrimRight(resp, "\r\n"), err
|
||||
}
|
||||
|
||||
// EncryptConfigFile encrypts json config data with a key
|
||||
func EncryptConfigFile(configData, key []byte) ([]byte, error) {
|
||||
sessionDK, salt, err := makeNewSessionDK(key)
|
||||
if err != nil {
|
||||
@@ -98,9 +108,14 @@ func EncryptConfigFile(configData, key []byte) ([]byte, error) {
|
||||
return c.encryptConfigFile(configData)
|
||||
}
|
||||
|
||||
// encryptConfigFile encrypts configuration data that is parsed in with a key
|
||||
// and returns it as a byte array with an error
|
||||
// encryptConfigFile encrypts json config data with a key
|
||||
// The EncryptConfig field is set to config enabled (1)
|
||||
func (c *Config) encryptConfigFile(configData []byte) ([]byte, error) {
|
||||
configData, err := jsonparser.Set(configData, []byte("1"), "encryptConfig")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrSettingEncryptConfig, err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(c.sessionDK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -115,42 +130,39 @@ func (c *Config) encryptConfigFile(configData []byte) ([]byte, error) {
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], configData)
|
||||
|
||||
appendedFile := []byte(EncryptConfirmString)
|
||||
appendedFile = append(appendedFile, c.storedSalt...)
|
||||
appendedFile := append(bytes.Clone(encryptionPrefix), c.storedSalt...)
|
||||
appendedFile = append(appendedFile, ciphertext...)
|
||||
return appendedFile, nil
|
||||
}
|
||||
|
||||
// DecryptConfigFile decrypts configuration data with the supplied key and
|
||||
// returns the un-encrypted data as a byte array with an error
|
||||
func DecryptConfigFile(configData, key []byte) ([]byte, error) {
|
||||
reader := bytes.NewReader(configData)
|
||||
return (&Config{}).decryptConfigData(reader, key)
|
||||
// DecryptConfigFile decrypts config data with a key
|
||||
func DecryptConfigFile(d, key []byte) ([]byte, error) {
|
||||
return (&Config{}).decryptConfigData(d, key)
|
||||
}
|
||||
|
||||
// decryptConfigData decrypts configuration data with the supplied key and
|
||||
// returns the un-encrypted data as a byte array with an error
|
||||
func (c *Config) decryptConfigData(configReader io.Reader, key []byte) ([]byte, error) {
|
||||
err := skipECS(configReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// decryptConfigData decrypts config data with a key
|
||||
func (c *Config) decryptConfigData(d, key []byte) ([]byte, error) {
|
||||
if !bytes.HasPrefix(d, encryptionPrefix) {
|
||||
return d, errNoPrefix
|
||||
}
|
||||
origKey := key
|
||||
configData, err := io.ReadAll(configReader)
|
||||
|
||||
d = bytes.TrimPrefix(d, encryptionPrefix)
|
||||
|
||||
sessionDK, storedSalt, err := makeNewSessionDK(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ConfirmSalt(configData) {
|
||||
salt := make([]byte, len(SaltPrefix)+SaltRandomLength)
|
||||
salt = configData[0:len(salt)]
|
||||
if bytes.HasPrefix(d, saltPrefix) {
|
||||
salt := make([]byte, len(saltPrefix)+saltRandomLength)
|
||||
salt = d[0:len(salt)]
|
||||
|
||||
key, err = getScryptDK(key, salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configData = configData[len(salt):]
|
||||
d = d[len(salt):]
|
||||
}
|
||||
|
||||
blockDecrypt, err := aes.NewCipher(key)
|
||||
@@ -158,58 +170,49 @@ func (c *Config) decryptConfigData(configReader io.Reader, key []byte) ([]byte,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(configData) < aes.BlockSize {
|
||||
return nil, errors.New(errAESBlockSize)
|
||||
if len(d) < aes.BlockSize {
|
||||
return nil, errAESBlockSize
|
||||
}
|
||||
|
||||
iv := configData[:aes.BlockSize]
|
||||
configData = configData[aes.BlockSize:]
|
||||
iv, d := d[:aes.BlockSize], d[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(blockDecrypt, iv)
|
||||
stream.XORKeyStream(configData, configData)
|
||||
result := configData
|
||||
stream.XORKeyStream(d, d)
|
||||
|
||||
sessionDK, storedSalt, err := makeNewSessionDK(origKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.sessionDK, c.storedSalt = sessionDK, storedSalt
|
||||
|
||||
return result, nil
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ConfirmSalt checks whether the encrypted data contains a salt
|
||||
func ConfirmSalt(file []byte) bool {
|
||||
return bytes.Contains(file, []byte(SaltPrefix))
|
||||
// IsEncrypted returns if the data sequence is encrypted
|
||||
func IsEncrypted(data []byte) bool {
|
||||
return bytes.HasPrefix(data, encryptionPrefix)
|
||||
}
|
||||
|
||||
// ConfirmECS confirms that the encryption confirmation string is found
|
||||
func ConfirmECS(file []byte) bool {
|
||||
return bytes.Contains(file, []byte(EncryptConfirmString))
|
||||
}
|
||||
|
||||
// skipECS skips encryption confirmation string
|
||||
// or errors, if the prefix wasn't found
|
||||
func skipECS(file io.Reader) error {
|
||||
buf := make([]byte, len(EncryptConfirmString))
|
||||
if _, err := io.ReadFull(file, buf); err != nil {
|
||||
return err
|
||||
// IsFileEncrypted returns if the file is encrypted
|
||||
// Returns false on error opening or reading
|
||||
func IsFileEncrypted(f string) bool {
|
||||
r, err := os.Open(f)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if string(buf) != EncryptConfirmString {
|
||||
return errors.New("data does not start with ECS")
|
||||
defer r.Close()
|
||||
prefix := make([]byte, len(encryptionPrefix))
|
||||
if _, err = io.ReadFull(r, prefix); err != nil {
|
||||
return false
|
||||
}
|
||||
return nil
|
||||
return bytes.Equal(prefix, encryptionPrefix)
|
||||
}
|
||||
|
||||
func getScryptDK(key, salt []byte) ([]byte, error) {
|
||||
if len(key) == 0 {
|
||||
return nil, errors.New("key is empty")
|
||||
return nil, errKeyIsEmpty
|
||||
}
|
||||
return scrypt.Key(key, salt, 32768, 8, 1, 32)
|
||||
}
|
||||
|
||||
func makeNewSessionDK(key []byte) (dk, storedSalt []byte, err error) {
|
||||
storedSalt, err = crypto.GetRandomSalt([]byte(SaltPrefix), SaltRandomLength)
|
||||
storedSalt, err = crypto.GetRandomSalt(saltPrefix, saltRandomLength)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -2,128 +2,110 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"crypto/aes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPromptForConfigEncryption(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
confirm, err := promptForConfigEncryption()
|
||||
if confirm {
|
||||
t.Error("promptForConfigEncryption return incorrect bool")
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("Expected error as there is no input")
|
||||
}
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
require.False(t, confirm)
|
||||
}
|
||||
|
||||
func TestPromptForConfigKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
byteyBite, err := PromptForConfigKey(true)
|
||||
if err == nil && len(byteyBite) > 1 {
|
||||
t.Errorf("PromptForConfigKey: %s", err)
|
||||
}
|
||||
withInteractiveResponse(t, "\n\n", func() {
|
||||
_, err := PromptForConfigKey(false)
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
})
|
||||
|
||||
_, err = PromptForConfigKey(false)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
withInteractiveResponse(t, "pass\n", func() {
|
||||
k, err := PromptForConfigKey(false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "pass", string(k))
|
||||
})
|
||||
|
||||
withInteractiveResponse(t, "what\nwhat\n", func() {
|
||||
k, err := PromptForConfigKey(true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "what", string(k))
|
||||
})
|
||||
|
||||
withInteractiveResponse(t, "what\nno\n", func() {
|
||||
_, err := PromptForConfigKey(true)
|
||||
require.ErrorIs(t, err, io.EOF, "PromptForConfigKey must EOF when asking for another input but none is given")
|
||||
})
|
||||
|
||||
withInteractiveResponse(t, "what\nno\nwhat\nno\nwhat\nno\n", func() {
|
||||
_, err := PromptForConfigKey(true)
|
||||
require.ErrorIs(t, err, io.EOF, "PromptForConfigKey must EOF when asking for another input but none is given")
|
||||
})
|
||||
|
||||
withInteractiveResponse(t, "what\nno\nwhat\nno\nwhat\nwhat\n", func() {
|
||||
k, err := PromptForConfigKey(true)
|
||||
require.NoError(t, err, "PromptForConfigKey must not error if the user eventually answers consistently")
|
||||
assert.Equal(t, "what", string(k))
|
||||
})
|
||||
}
|
||||
|
||||
func TestEncryptConfigFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := EncryptConfigFile([]byte("test"), nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
require.ErrorIs(t, err, errKeyIsEmpty)
|
||||
|
||||
c := &Config{
|
||||
sessionDK: []byte("a"),
|
||||
}
|
||||
_, err = c.encryptConfigFile([]byte("test"))
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
_, err = c.encryptConfigFile([]byte(`test`))
|
||||
require.ErrorIs(t, err, ErrSettingEncryptConfig)
|
||||
|
||||
_, err = c.encryptConfigFile([]byte(`{"test":1}`))
|
||||
require.Error(t, err)
|
||||
require.IsType(t, aes.KeySizeError(1), err)
|
||||
|
||||
sessDk, salt, err := makeNewSessionDK([]byte("asdf"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err, "makeNewSessionDK must not error")
|
||||
|
||||
c = &Config{
|
||||
sessionDK: sessDk,
|
||||
storedSalt: salt,
|
||||
}
|
||||
_, err = c.encryptConfigFile([]byte("test"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = c.encryptConfigFile([]byte(`{"test":1}`))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecryptConfigFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
result, err := EncryptConfigFile([]byte("test"), []byte("key"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, err := EncryptConfigFile([]byte(`{"test":1}`), []byte("key"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = DecryptConfigFile(result, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
d, err := DecryptConfigFile(e, []byte("key"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"test":1,"encryptConfig":1}`, string(d), "encryptConfig should be set to 1 after first encryption")
|
||||
|
||||
_, err = DecryptConfigFile(e, nil)
|
||||
require.ErrorIs(t, err, errKeyIsEmpty)
|
||||
|
||||
_, err = DecryptConfigFile([]byte("test"), nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
require.ErrorIs(t, err, errNoPrefix)
|
||||
|
||||
_, err = DecryptConfigFile([]byte("test"), []byte("AAAAAAAAAAAAAAAA"))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected %s", errAESBlockSize)
|
||||
}
|
||||
|
||||
result, err = EncryptConfigFile([]byte("test"), []byte("key"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = DecryptConfigFile(result, []byte("key"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = DecryptConfigFile(encryptionPrefix, []byte("AAAAAAAAAAAAAAAA"))
|
||||
require.ErrorIs(t, err, errAESBlockSize)
|
||||
}
|
||||
|
||||
func TestConfirmECS(t *testing.T) {
|
||||
func TestIsEncrypted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ECStest := []byte(EncryptConfirmString)
|
||||
if !ConfirmECS(ECStest) {
|
||||
t.Errorf("TestConfirmECS: Error finding ECS.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveECS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ECStest := []byte(EncryptConfirmString)
|
||||
reader := bytes.NewReader(ECStest)
|
||||
err := skipECS(reader)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Attempt read
|
||||
var buf []byte
|
||||
_, err = reader.Read(buf)
|
||||
if err != io.EOF {
|
||||
t.Errorf("TestConfirmECS: Error ECS not deleted.")
|
||||
}
|
||||
assert.True(t, IsEncrypted(encryptionPrefix))
|
||||
assert.False(t, IsEncrypted([]byte("mhmmm. Donuts.")))
|
||||
}
|
||||
|
||||
func TestMakeNewSessionDK(t *testing.T) {
|
||||
@@ -189,7 +171,7 @@ func TestEncryptTwiceReusesSaltButNewCipher(t *testing.T) {
|
||||
t.Fatalf("Problem reading file %s: %s\n", enc2, err)
|
||||
}
|
||||
// length of prefix + salt
|
||||
l := len(EncryptConfirmString+SaltPrefix) + SaltRandomLength
|
||||
l := len(encryptionPrefix) + len(saltPrefix) + saltRandomLength
|
||||
// Even though prefix, including salt with the random bytes is the same
|
||||
if !bytes.Equal(data1[:l], data2[:l]) {
|
||||
t.Error("Salt is not reused.")
|
||||
@@ -208,44 +190,20 @@ func TestSaveAndReopenEncryptedConfig(t *testing.T) {
|
||||
|
||||
// Save encrypted config
|
||||
enc := filepath.Join(tempDir, "encrypted.dat")
|
||||
err := withInteractiveResponse(t, "pass\npass\n", func() error {
|
||||
return c.SaveConfigToFile(enc)
|
||||
withInteractiveResponse(t, "pass\npass\n", func() {
|
||||
err := c.SaveConfigToFile(enc)
|
||||
require.NoError(t, err, "SaveConfigToFile must not error")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Problem storing config in file %s: %s\n", enc, err)
|
||||
}
|
||||
|
||||
readConf := &Config{}
|
||||
err = withInteractiveResponse(t, "pass\n", func() error {
|
||||
withInteractiveResponse(t, "pass\n", func() {
|
||||
// Load with no existing state, key is read from the prepared file
|
||||
return readConf.ReadConfigFromFile(enc, true)
|
||||
err := readConf.ReadConfigFromFile(enc, true)
|
||||
require.NoError(t, err, "ReadConfigFromFile must not error")
|
||||
})
|
||||
|
||||
// Verify
|
||||
if err != nil {
|
||||
t.Fatalf("Problem reading config in file %s: %s\n", enc, err)
|
||||
}
|
||||
|
||||
if c.Name != readConf.Name || c.EncryptConfig != readConf.EncryptConfig {
|
||||
t.Error("Loaded conf not the same as original")
|
||||
}
|
||||
}
|
||||
|
||||
// setAnswersFile sets the given file as the current stdin
|
||||
// returns the close function to defer for reverting the stdin
|
||||
func setAnswersFile(t *testing.T, answerFile string) func() {
|
||||
t.Helper()
|
||||
oldIn := os.Stdin
|
||||
|
||||
inputFile, err := os.Open(answerFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Problem opening temp file at %s: %s\n", answerFile, err)
|
||||
}
|
||||
os.Stdin = inputFile
|
||||
return func() {
|
||||
inputFile.Close()
|
||||
os.Stdin = oldIn
|
||||
}
|
||||
assert.Equal(t, "myCustomName", readConf.Name, "Name should be correct")
|
||||
assert.Equal(t, 1, readConf.EncryptConfig, "EncryptConfig should be set correctly")
|
||||
}
|
||||
|
||||
func TestReadConfigWithPrompt(t *testing.T) {
|
||||
@@ -260,14 +218,13 @@ func TestReadConfigWithPrompt(t *testing.T) {
|
||||
// Save config
|
||||
testConfigFile := filepath.Join(tempDir, "config.json")
|
||||
err := c.SaveConfigToFile(testConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Problem saving config file in %s: %s\n", tempDir, err)
|
||||
}
|
||||
require.NoError(t, err, "SaveConfigToFile must not error")
|
||||
|
||||
// Run the test
|
||||
c = &Config{}
|
||||
err = withInteractiveResponse(t, "y\npass\npass\n", func() error {
|
||||
return c.ReadConfigFromFile(testConfigFile, false)
|
||||
withInteractiveResponse(t, "y\npass\npass\n", func() {
|
||||
err = c.ReadConfigFromFile(testConfigFile, false)
|
||||
require.NoError(t, err, "ReadConfigFromFile must not error")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Problem reading config file at %s: %s\n", testConfigFile, err)
|
||||
@@ -281,33 +238,24 @@ func TestReadConfigWithPrompt(t *testing.T) {
|
||||
if c.EncryptConfig != fileEncryptionEnabled {
|
||||
t.Error("Config encryption flag should be set after prompts")
|
||||
}
|
||||
if !ConfirmECS(data) {
|
||||
t.Error("Config file should be encrypted after prompts")
|
||||
}
|
||||
assert.True(t, IsEncrypted(data), "data should be encrypted after prompts")
|
||||
}
|
||||
|
||||
func TestReadEncryptedConfigFromReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
keyProvider := func() ([]byte, error) { return []byte("pass"), nil }
|
||||
c := &Config{
|
||||
EncryptionKeyProvider: func(_ bool) ([]byte, error) { return []byte("pass"), nil },
|
||||
}
|
||||
// Encrypted conf for: `{"name":"test"}` with key `pass`
|
||||
confBytes := []byte{84, 72, 79, 82, 83, 45, 72, 65, 77, 77, 69, 82, 126, 71, 67, 84, 126, 83, 79, 126, 83, 65, 76, 84, 89, 126, 246, 110, 128, 3, 30, 168, 172, 160, 198, 176, 136, 62, 152, 155, 253, 176, 16, 48, 52, 246, 44, 29, 151, 47, 217, 226, 178, 12, 218, 113, 248, 172, 195, 232, 136, 104, 9, 199, 20, 4, 71, 4, 253, 249}
|
||||
conf, encrypted, err := ReadConfig(bytes.NewReader(confBytes), keyProvider)
|
||||
if err != nil {
|
||||
t.Errorf("TestReadConfig %s", err)
|
||||
}
|
||||
if !encrypted {
|
||||
t.Errorf("Expected encrypted config %s", err)
|
||||
}
|
||||
if conf.Name != "test" {
|
||||
t.Errorf("Conf not properly loaded %s", err)
|
||||
}
|
||||
err := c.readConfig(bytes.NewReader(confBytes))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test", c.Name)
|
||||
|
||||
// Change the salt
|
||||
confBytes[20] = 0
|
||||
conf, _, err = ReadConfig(bytes.NewReader(confBytes), keyProvider)
|
||||
if err == nil {
|
||||
t.Errorf("Expected unable to decrypt, but got %+v", conf)
|
||||
}
|
||||
err = c.readConfig(bytes.NewReader(confBytes))
|
||||
require.ErrorIs(t, err, errDecryptFailed)
|
||||
}
|
||||
|
||||
// TestSaveConfigToFileWithErrorInPasswordPrompt should preserve the original file
|
||||
@@ -318,58 +266,36 @@ func TestSaveConfigToFileWithErrorInPasswordPrompt(t *testing.T) {
|
||||
}
|
||||
testData := []byte("testdata")
|
||||
f, err := os.CreateTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err, "CreateTemp must not error")
|
||||
|
||||
targetFile := f.Name()
|
||||
defer os.Remove(targetFile)
|
||||
|
||||
_, err = io.Copy(f, bytes.NewReader(testData))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = withInteractiveResponse(t, "\n\n", func() error {
|
||||
require.NoError(t, err, "io.Copy must not error")
|
||||
require.NoError(t, f.Close(), "file Close must not error")
|
||||
|
||||
withInteractiveResponse(t, "\n\n", func() {
|
||||
err = c.SaveConfigToFile(targetFile)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
return nil
|
||||
require.ErrorIs(t, err, io.EOF, "SaveConfigToFile must not error")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(targetFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(data, testData) {
|
||||
t.Errorf("Expected contents %s, but was %s", testData, data)
|
||||
}
|
||||
require.NoError(t, err, "ReadFile must not error")
|
||||
assert.Equal(t, testData, data)
|
||||
}
|
||||
|
||||
func withInteractiveResponse(t *testing.T, response string, body func() error) error {
|
||||
t.Helper()
|
||||
// Answers to the prompt
|
||||
responseFile, err := os.CreateTemp("", "*.in")
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem creating temp file: %w", err)
|
||||
}
|
||||
_, err = responseFile.WriteString(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem writing to temp file at %s: %w", responseFile.Name(), err)
|
||||
}
|
||||
err = responseFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem closing temp file at %s: %w", responseFile.Name(), err)
|
||||
}
|
||||
defer os.Remove(responseFile.Name())
|
||||
|
||||
// Temporarily replace Stdin with a custom input
|
||||
cleanup := setAnswersFile(t, responseFile.Name())
|
||||
defer cleanup()
|
||||
return body()
|
||||
func withInteractiveResponse(tb testing.TB, response string, fn func()) {
|
||||
tb.Helper()
|
||||
f, err := os.CreateTemp("", "*.in")
|
||||
require.NoError(tb, err, "CreateTemp must not error")
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
_, err = f.WriteString(response)
|
||||
require.NoError(tb, err, "WriteString must not error")
|
||||
_, err = f.Seek(0, 0)
|
||||
require.NoError(tb, err, "Seek must not error")
|
||||
defer func(orig *os.File) { os.Stdin = orig }(os.Stdin)
|
||||
os.Stdin = f
|
||||
fn()
|
||||
}
|
||||
|
||||
@@ -93,13 +93,9 @@ func TestGetExchangeBankAccounts(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
_, err := cfg.GetExchangeBankAccounts(bfx, "", "USD")
|
||||
if err != nil {
|
||||
t.Error("GetExchangeBankAccounts error", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
_, err = cfg.GetExchangeBankAccounts("Not an exchange", "", "Not a currency")
|
||||
if err == nil {
|
||||
t.Error("GetExchangeBankAccounts, no error returned for invalid exchange")
|
||||
}
|
||||
require.ErrorIs(t, err, ErrExchangeNotFound)
|
||||
}
|
||||
|
||||
func TestCheckBankAccountConfig(t *testing.T) {
|
||||
@@ -1310,18 +1306,8 @@ func TestCheckExchangeConfigValues(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg.Exchanges[0].Name = "GDAX"
|
||||
err = cfg.CheckExchangeConfigValues()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if cfg.Exchanges[0].Name != "CoinbasePro" {
|
||||
t.Error("exchange name should have been updated from GDAX to CoinbasePRo")
|
||||
}
|
||||
|
||||
// Test API settings migration
|
||||
sptr := func(s string) *string { return &s }
|
||||
int64ptr := func(i int64) *int64 { return &i }
|
||||
|
||||
cfg.Exchanges[0].APIKey = sptr("awesomeKey")
|
||||
cfg.Exchanges[0].APISecret = sptr("meowSecret")
|
||||
@@ -1378,96 +1364,6 @@ func TestCheckExchangeConfigValues(t *testing.T) {
|
||||
t.Error("unexpected values")
|
||||
}
|
||||
|
||||
p1, err := currency.NewPairDelimiter(testPair, "-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test currency pair migration
|
||||
setupPairs := func(emptyAssets bool) {
|
||||
cfg.Exchanges[0].CurrencyPairs = nil
|
||||
p := currency.Pairs{
|
||||
p1,
|
||||
}
|
||||
cfg.Exchanges[0].PairsLastUpdated = int64ptr(1234567)
|
||||
|
||||
if !emptyAssets {
|
||||
cfg.Exchanges[0].AssetTypes = sptr("spot")
|
||||
}
|
||||
|
||||
cfg.Exchanges[0].AvailablePairs = &p
|
||||
cfg.Exchanges[0].EnabledPairs = &p
|
||||
cfg.Exchanges[0].ConfigCurrencyPairFormat = ¤cy.PairFormat{
|
||||
Uppercase: true,
|
||||
Delimiter: "-",
|
||||
}
|
||||
cfg.Exchanges[0].RequestCurrencyPairFormat = ¤cy.PairFormat{
|
||||
Uppercase: false,
|
||||
Delimiter: "~",
|
||||
}
|
||||
}
|
||||
|
||||
setupPairs(false)
|
||||
err = cfg.CheckExchangeConfigValues()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
setupPairs(true)
|
||||
err = cfg.CheckExchangeConfigValues()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if cfg.Exchanges[0].CurrencyPairs.LastUpdated != 1234567 {
|
||||
t.Error("last updated has wrong value")
|
||||
}
|
||||
|
||||
pFmt := cfg.Exchanges[0].CurrencyPairs.ConfigFormat
|
||||
if pFmt.Delimiter != "-" ||
|
||||
!pFmt.Uppercase {
|
||||
t.Error("unexpected config format values")
|
||||
}
|
||||
|
||||
pFmt = cfg.Exchanges[0].CurrencyPairs.RequestFormat
|
||||
if pFmt.Delimiter != "~" ||
|
||||
pFmt.Uppercase {
|
||||
t.Error("unexpected request format values")
|
||||
}
|
||||
|
||||
if !cfg.Exchanges[0].CurrencyPairs.GetAssetTypes(false).Contains(asset.Spot) ||
|
||||
!cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat {
|
||||
t.Error("unexpected results")
|
||||
}
|
||||
|
||||
pairs, err := cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(pairs) == 0 || pairs.Join() != testPair {
|
||||
t.Error("pairs not set properly")
|
||||
}
|
||||
|
||||
pairs, err = cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(pairs) == 0 || pairs.Join() != testPair {
|
||||
t.Error("pairs not set properly")
|
||||
}
|
||||
|
||||
// Ensure that all old settings are flushed
|
||||
if cfg.Exchanges[0].PairsLastUpdated != nil ||
|
||||
cfg.Exchanges[0].ConfigCurrencyPairFormat != nil ||
|
||||
cfg.Exchanges[0].RequestCurrencyPairFormat != nil ||
|
||||
cfg.Exchanges[0].AssetTypes != nil ||
|
||||
cfg.Exchanges[0].AvailablePairs != nil ||
|
||||
cfg.Exchanges[0].EnabledPairs != nil {
|
||||
t.Error("unexpected results")
|
||||
}
|
||||
|
||||
// Test AutoPairUpdates
|
||||
cfg.Exchanges[0].Features.Supports.RESTCapabilities.AutoPairUpdates = false
|
||||
cfg.Exchanges[0].Features.Supports.WebsocketCapabilities.AutoPairUpdates = false
|
||||
@@ -1644,15 +1540,11 @@ func TestCheckExchangeConfigValues(t *testing.T) {
|
||||
cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = nil
|
||||
cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].AssetEnabled = convert.BoolPtr(false)
|
||||
err = cfg.CheckExchangeConfigValues()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg.Exchanges[0].CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
|
||||
err = cfg.CheckExchangeConfigValues()
|
||||
if err == nil {
|
||||
t.Error("err cannot be nil")
|
||||
}
|
||||
assert.ErrorIs(t, err, errNoEnabledExchanges, "Exchanges without any pairs should be disabled")
|
||||
}
|
||||
|
||||
func TestReadConfigFromFile(t *testing.T) {
|
||||
@@ -1670,22 +1562,14 @@ func TestReadConfigFromFile(t *testing.T) {
|
||||
|
||||
func TestReadConfigFromReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Config{}
|
||||
confString := `{"name":"test"}`
|
||||
conf, encrypted, err := ReadConfig(strings.NewReader(confString), Unencrypted)
|
||||
if err != nil {
|
||||
t.Errorf("TestReadConfig %s", err)
|
||||
}
|
||||
if encrypted {
|
||||
t.Errorf("Expected unencrypted config %s", err)
|
||||
}
|
||||
if conf.Name != "test" {
|
||||
t.Errorf("Conf not properly loaded %s", err)
|
||||
}
|
||||
err := c.readConfig(strings.NewReader(confString))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test", c.Name)
|
||||
|
||||
_, _, err = ReadConfig(strings.NewReader("{}"), Unencrypted)
|
||||
if err == nil {
|
||||
t.Error("TestReadConfig error cannot be nil")
|
||||
}
|
||||
err = c.readConfig(strings.NewReader("{}"))
|
||||
require.NoError(t, err, "Reading a config shorter than encryptionPrefix must not error EOF")
|
||||
}
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
@@ -2057,10 +1941,8 @@ func TestCheckCurrencyConfigValues(t *testing.T) {
|
||||
|
||||
func TestPreengineConfigUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
var c Config
|
||||
if err := c.LoadConfig("../testdata/preengine_config.json", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := new(Config).LoadConfig("../testdata/preengine_config.json", false)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoveExchange(t *testing.T) {
|
||||
@@ -2138,75 +2020,59 @@ func TestMigrateConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T)
|
||||
cleanup func(t *testing.T)
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nonexisting",
|
||||
args: args{
|
||||
configFile: "not-exists.json",
|
||||
},
|
||||
wantErr: true,
|
||||
wantErr: os.ErrNotExist,
|
||||
},
|
||||
{
|
||||
name: "source present, no target dir",
|
||||
setup: func(t *testing.T) {
|
||||
t.Helper()
|
||||
test, err := os.Create("test.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
test.Close()
|
||||
},
|
||||
cleanup: func(t *testing.T) {
|
||||
t.Helper()
|
||||
os.Remove("test.json")
|
||||
test, err := os.Create(filepath.Join(dir, "test.json"))
|
||||
require.NoError(t, err, "os.Create must not error")
|
||||
require.NoError(t, test.Close(), "file Close must not error")
|
||||
},
|
||||
args: args{
|
||||
configFile: "test.json",
|
||||
configFile: filepath.Join(dir, "test.json"),
|
||||
targetDir: filepath.Join(dir, "new"),
|
||||
},
|
||||
want: filepath.Join(dir, "new", File),
|
||||
wantErr: false,
|
||||
want: filepath.Join(dir, "new", File),
|
||||
},
|
||||
{
|
||||
name: "source same as target",
|
||||
setup: func(t *testing.T) {
|
||||
t.Helper()
|
||||
err := file.Write(filepath.Join(dir, File), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err, "file.Write must not error")
|
||||
},
|
||||
args: args{
|
||||
configFile: filepath.Join(dir, File),
|
||||
targetDir: dir,
|
||||
},
|
||||
want: filepath.Join(dir, File),
|
||||
wantErr: false,
|
||||
want: filepath.Join(dir, File),
|
||||
},
|
||||
{
|
||||
name: "source and target present",
|
||||
setup: func(t *testing.T) {
|
||||
t.Helper()
|
||||
err := file.Write(filepath.Join(dir, File), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err, "file.Write must not error")
|
||||
err = file.Write(filepath.Join(dir, "src", EncryptedFile), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err, "file.Write must not error")
|
||||
},
|
||||
args: args{
|
||||
configFile: filepath.Join(dir, "src", EncryptedFile),
|
||||
targetDir: dir,
|
||||
},
|
||||
want: filepath.Join(dir, "src", EncryptedFile),
|
||||
// We only expect warning
|
||||
wantErr: false,
|
||||
want: filepath.Join(dir, "src", EncryptedFile),
|
||||
wantErr: nil, // We only expect warning
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2215,19 +2081,13 @@ func TestMigrateConfig(t *testing.T) {
|
||||
if tt.setup != nil {
|
||||
tt.setup(t)
|
||||
}
|
||||
if tt.cleanup != nil {
|
||||
defer tt.cleanup(t)
|
||||
}
|
||||
got, err := migrateConfig(tt.args.configFile, tt.args.targetDir)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("migrateConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("migrateConfig() = %v, want %v", got, tt.want)
|
||||
}
|
||||
if err == nil && !file.Exists(got) {
|
||||
t.Errorf("migrateConfig: %v should exist", got)
|
||||
if tt.wantErr != nil {
|
||||
require.ErrorIs(t, err, tt.wantErr, "migrateConfig must error correctly")
|
||||
} else {
|
||||
require.NoError(t, err, "migrateConfig must not error")
|
||||
require.Equal(t, tt.want, got, "migrateConfig must return the correct file")
|
||||
require.Truef(t, file.Exists(got), "migrateConfig return file `%s` must exist", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,12 +58,8 @@ const (
|
||||
|
||||
// Constants here hold some messages
|
||||
const (
|
||||
ErrExchangeNameEmpty = "exchange #%d name is empty"
|
||||
ErrNoEnabledExchanges = "no exchanges enabled"
|
||||
ErrFailureOpeningConfig = "fatal error opening %s file. Error: %s"
|
||||
ErrCheckingConfigValues = "fatal error checking config values. Error: %s"
|
||||
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!"
|
||||
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
|
||||
@@ -78,19 +74,24 @@ const (
|
||||
|
||||
// Public errors exported by this package
|
||||
var (
|
||||
ErrExchangeNotFound = errors.New("exchange not found")
|
||||
ErrExchangeNotFound = errors.New("exchange not found")
|
||||
ErrFailureOpeningConfig = errors.New("fatal error opening file")
|
||||
)
|
||||
|
||||
var (
|
||||
cfg Config
|
||||
m sync.Mutex
|
||||
|
||||
errNoEnabledExchanges = errors.New("no exchanges enabled")
|
||||
errCheckingConfigValues = errors.New("fatal error checking config values")
|
||||
errExchangeNameEmpty = errors.New("exchange name is empty")
|
||||
)
|
||||
|
||||
// Config is the overarching object that holds all the information for
|
||||
// prestart management of Portfolio, Communications, Webserver and Enabled
|
||||
// Exchanges
|
||||
// prestart management of Portfolio, Communications, Webserver and Enabled Exchanges
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
Version int `json:"version"`
|
||||
DataDirectory string `json:"dataDirectory"`
|
||||
EncryptConfig int `json:"encryptConfig"`
|
||||
GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"`
|
||||
@@ -118,10 +119,14 @@ type Config struct {
|
||||
Cryptocurrencies *currency.Currencies `json:"cryptocurrencies,omitempty"`
|
||||
SMS *base.SMSGlobalConfig `json:"smsGlobal,omitempty"`
|
||||
// encryption session values
|
||||
storedSalt []byte
|
||||
sessionDK []byte
|
||||
storedSalt []byte
|
||||
sessionDK []byte
|
||||
EncryptionKeyProvider EncryptionKeyProvider `json:"-"`
|
||||
}
|
||||
|
||||
// EncryptionKeyProvider is a function config can use to prompt the user for an encryption key
|
||||
type EncryptionKeyProvider func(confirmKey bool) ([]byte, error)
|
||||
|
||||
// OrderManager holds settings used for the order manager
|
||||
type OrderManager struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
@@ -197,24 +202,18 @@ type Exchange struct {
|
||||
Orderbook Orderbook `json:"orderbook"`
|
||||
|
||||
// Deprecated settings which will be removed in a future update
|
||||
AvailablePairs *currency.Pairs `json:"availablePairs,omitempty"`
|
||||
EnabledPairs *currency.Pairs `json:"enabledPairs,omitempty"`
|
||||
AssetTypes *string `json:"assetTypes,omitempty"`
|
||||
PairsLastUpdated *int64 `json:"pairsLastUpdated,omitempty"`
|
||||
ConfigCurrencyPairFormat *currency.PairFormat `json:"configCurrencyPairFormat,omitempty"`
|
||||
RequestCurrencyPairFormat *currency.PairFormat `json:"requestCurrencyPairFormat,omitempty"`
|
||||
AuthenticatedAPISupport *bool `json:"authenticatedApiSupport,omitempty"`
|
||||
AuthenticatedWebsocketAPISupport *bool `json:"authenticatedWebsocketApiSupport,omitempty"`
|
||||
APIKey *string `json:"apiKey,omitempty"`
|
||||
APISecret *string `json:"apiSecret,omitempty"`
|
||||
APIAuthPEMKeySupport *bool `json:"apiAuthPemKeySupport,omitempty"`
|
||||
APIAuthPEMKey *string `json:"apiAuthPemKey,omitempty"`
|
||||
APIURL *string `json:"apiUrl,omitempty"`
|
||||
APIURLSecondary *string `json:"apiUrlSecondary,omitempty"`
|
||||
ClientID *string `json:"clientId,omitempty"`
|
||||
SupportsAutoPairUpdates *bool `json:"supportsAutoPairUpdates,omitempty"`
|
||||
Websocket *bool `json:"websocket,omitempty"`
|
||||
WebsocketURL *string `json:"websocketUrl,omitempty"`
|
||||
AuthenticatedAPISupport *bool `json:"authenticatedApiSupport,omitempty"`
|
||||
AuthenticatedWebsocketAPISupport *bool `json:"authenticatedWebsocketApiSupport,omitempty"`
|
||||
APIKey *string `json:"apiKey,omitempty"`
|
||||
APISecret *string `json:"apiSecret,omitempty"`
|
||||
APIAuthPEMKeySupport *bool `json:"apiAuthPemKeySupport,omitempty"`
|
||||
APIAuthPEMKey *string `json:"apiAuthPemKey,omitempty"`
|
||||
APIURL *string `json:"apiUrl,omitempty"`
|
||||
APIURLSecondary *string `json:"apiUrlSecondary,omitempty"`
|
||||
ClientID *string `json:"clientId,omitempty"`
|
||||
SupportsAutoPairUpdates *bool `json:"supportsAutoPairUpdates,omitempty"`
|
||||
Websocket *bool `json:"websocket,omitempty"`
|
||||
WebsocketURL *string `json:"websocketUrl,omitempty"`
|
||||
}
|
||||
|
||||
// Profiler defines the profiler configuration to enable pprof
|
||||
|
||||
57
config/versions/fixtures_test.go
Normal file
57
config/versions/fixtures_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// TestVersion1 is an empty and incompatible Version for testing
|
||||
type TestVersion1 struct{}
|
||||
|
||||
// TestVersion2 is test fixture
|
||||
type TestVersion2 struct {
|
||||
ConfigErr bool
|
||||
ExchErr bool
|
||||
}
|
||||
|
||||
var (
|
||||
errUpgrade = errors.New("do you expect me to talk?")
|
||||
errDowngrade = errors.New("no, I expect you to die")
|
||||
)
|
||||
|
||||
// UpgradeConfig errors if v.ConfigErr is true
|
||||
func (v *TestVersion2) UpgradeConfig(_ context.Context, c []byte) ([]byte, error) {
|
||||
if v.ConfigErr {
|
||||
return c, errUpgrade
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DowngradeConfig errors if v.ConfigErr is true
|
||||
func (v *TestVersion2) DowngradeConfig(_ context.Context, c []byte) ([]byte, error) {
|
||||
if v.ConfigErr {
|
||||
return c, errDowngrade
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Exchanges returns just Juan
|
||||
func (v *TestVersion2) Exchanges() []string {
|
||||
return []string{"Juan"}
|
||||
}
|
||||
|
||||
// UpgradeExchange errors if ExchErr is true
|
||||
func (v *TestVersion2) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
|
||||
if v.ExchErr {
|
||||
return e, errUpgrade
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// DowngradeExchange errors if ExchErr is true
|
||||
func (v *TestVersion2) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
|
||||
if v.ExchErr {
|
||||
return e, errDowngrade
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
24
config/versions/v0.go
Normal file
24
config/versions/v0.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Version0 is a baseline version with no changes, so we can downgrade back to nothing
|
||||
// It does not implement any upgrade interfaces
|
||||
type Version0 struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Manager.registerVersion(0, &Version0{})
|
||||
}
|
||||
|
||||
// UpgradeConfig is an empty stub
|
||||
func (v *Version0) UpgradeConfig(_ context.Context, j []byte) ([]byte, error) {
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// DowngradeConfig is an empty stub
|
||||
func (v *Version0) DowngradeConfig(_ context.Context, j []byte) ([]byte, error) {
|
||||
return j, nil
|
||||
}
|
||||
17
config/versions/v0/types.go
Normal file
17
config/versions/v0/types.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package v0
|
||||
|
||||
// Exchange contains a sub-section of exchange config
|
||||
type Exchange struct {
|
||||
AvailablePairs string `json:"availablePairs,omitempty"`
|
||||
EnabledPairs string `json:"enabledPairs,omitempty"`
|
||||
PairsLastUpdated int64 `json:"pairsLastUpdated,omitempty"`
|
||||
ConfigCurrencyPairFormat *PairFormat `json:"configCurrencyPairFormat,omitempty"`
|
||||
RequestCurrencyPairFormat *PairFormat `json:"requestCurrencyPairFormat,omitempty"`
|
||||
}
|
||||
|
||||
// PairFormat contains pair formatting config
|
||||
type PairFormat struct {
|
||||
Uppercase bool `json:"uppercase"`
|
||||
Delimiter string `json:"delimiter,omitempty"`
|
||||
Separator string `json:"separator,omitempty"`
|
||||
}
|
||||
59
config/versions/v1.go
Normal file
59
config/versions/v1.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
|
||||
v1 "github.com/thrasher-corp/gocryptotrader/config/versions/v1"
|
||||
)
|
||||
|
||||
// Version1 is an ExchangeVersion to upgrade currency pair format for exchanges
|
||||
type Version1 struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Manager.registerVersion(1, &Version1{})
|
||||
}
|
||||
|
||||
// Exchanges returns all exchanges: "*"
|
||||
func (v *Version1) Exchanges() []string { return []string{"*"} }
|
||||
|
||||
// UpgradeExchange will upgrade currency pair format
|
||||
func (v *Version1) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
|
||||
if _, d, _, err := jsonparser.Get(e, "currencyPairs"); err == nil && d == jsonparser.Object {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
d := &v0.Exchange{}
|
||||
if err := json.Unmarshal(e, d); err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
p := &v1.PairsManager{
|
||||
UseGlobalFormat: true,
|
||||
LastUpdated: d.PairsLastUpdated,
|
||||
ConfigFormat: d.ConfigCurrencyPairFormat,
|
||||
RequestFormat: d.RequestCurrencyPairFormat,
|
||||
Pairs: v1.FullStore{
|
||||
"spot": {
|
||||
Available: d.AvailablePairs,
|
||||
Enabled: d.EnabledPairs,
|
||||
},
|
||||
},
|
||||
}
|
||||
j, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
for _, f := range []string{"pairsLastUpdated", "configCurrencyPairFormat", "requestCurrencyPairFormat", "assetTypes", "availablePairs", "enabledPairs"} {
|
||||
e = jsonparser.Delete(e, f)
|
||||
}
|
||||
return jsonparser.Set(e, j, "currencyPairs")
|
||||
}
|
||||
|
||||
// DowngradeExchange doesn't do anything for v1; There's no downgrade path since the original state is lossy and v1 was before versioning
|
||||
func (v *Version1) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
|
||||
return e, nil
|
||||
}
|
||||
21
config/versions/v1/types.go
Normal file
21
config/versions/v1/types.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package v1
|
||||
|
||||
import v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
|
||||
|
||||
// PairsManager contains exchange pair management config
|
||||
type PairsManager struct {
|
||||
BypassConfigFormatUpgrades bool `json:"bypassConfigFormatUpgrades"`
|
||||
RequestFormat *v0.PairFormat `json:"requestFormat,omitempty"`
|
||||
ConfigFormat *v0.PairFormat `json:"configFormat,omitempty"`
|
||||
UseGlobalFormat bool `json:"useGlobalFormat,omitempty"`
|
||||
LastUpdated int64 `json:"lastUpdated,omitempty"`
|
||||
Pairs FullStore `json:"pairs"`
|
||||
}
|
||||
|
||||
// FullStore contains a pair store by asset name
|
||||
type FullStore map[string]struct {
|
||||
Enabled string `json:"enabled"`
|
||||
Available string `json:"available"`
|
||||
RequestFormat *v0.PairFormat `json:"requestFormat,omitempty"`
|
||||
ConfigFormat *v0.PairFormat `json:"configFormat,omitempty"`
|
||||
}
|
||||
31
config/versions/v1_test.go
Normal file
31
config/versions/v1_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVersion1Upgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := &Version1{}
|
||||
in := []byte(`{"name":"Wibble","pairsLastUpdated":1566798411,"assetTypes":"spot","configCurrencyPairFormat":{"uppercase":true,"delimiter":"_"},"requestCurrencyPairFormat":{"uppercase":false,"delimiter":"_","separator":"-"},"enabledPairs":"LTC_BTC","availablePairs":"LTC_BTC,ETH_BTC,BTC_USD"}`)
|
||||
exp := []byte(`{"name":"Wibble","currencyPairs":{"bypassConfigFormatUpgrades":false,"requestFormat":{"uppercase":false,"delimiter":"_","separator":"-"},"configFormat":{"uppercase":true,"delimiter":"_"},"useGlobalFormat":true,"lastUpdated":1566798411,"pairs":{"spot":{"enabled":"LTC_BTC","available":"LTC_BTC,ETH_BTC,BTC_USD"}}}}`)
|
||||
|
||||
out, err := v.UpgradeExchange(context.Background(), in)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, out)
|
||||
assert.Equal(t, string(exp), string(out))
|
||||
}
|
||||
|
||||
func TestVersion1Downgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := []byte("just leave me alone, mkay?")
|
||||
out, err := new(Version1).DowngradeExchange(context.Background(), bytes.Clone(in))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, out, in)
|
||||
}
|
||||
38
config/versions/v2.go
Normal file
38
config/versions/v2.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
)
|
||||
|
||||
// Version2 is an ExchangeVersion to change the name of GDAX to CoinbasePro
|
||||
type Version2 struct{}
|
||||
|
||||
func init() {
|
||||
Manager.registerVersion(2, &Version2{})
|
||||
}
|
||||
|
||||
const (
|
||||
from = "GDAX"
|
||||
to = "CoinbasePro"
|
||||
)
|
||||
|
||||
// Exchanges returns just GDAX and CoinbasePro
|
||||
func (v *Version2) Exchanges() []string { return []string{from, to} }
|
||||
|
||||
// UpgradeExchange will change the exchange name from GDAX to CoinbasePro
|
||||
func (v *Version2) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
|
||||
if n, err := jsonparser.GetString(e, "name"); err == nil && n == from {
|
||||
return jsonparser.Set(e, []byte(`"`+to+`"`), "name")
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// DowngradeExchange will change the exchange name from CoinbasePro to GDAX
|
||||
func (v *Version2) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
|
||||
if n, err := jsonparser.GetString(e, "name"); err == nil && n == to {
|
||||
return jsonparser.Set(e, []byte(`"`+from+`"`), "name")
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
37
config/versions/v2_test.go
Normal file
37
config/versions/v2_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVersion2Upgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tt := range [][]string{
|
||||
{"GDAX", "CoinbasePro"},
|
||||
{"Kraken", "Kraken"},
|
||||
{"CoinbasePro", "CoinbasePro"},
|
||||
} {
|
||||
out, err := new(Version2).UpgradeExchange(context.Background(), []byte(`{"name":"`+tt[0]+`"}`))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, out)
|
||||
assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion2Downgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tt := range [][]string{
|
||||
{"GDAX", "GDAX"},
|
||||
{"Kraken", "Kraken"},
|
||||
{"CoinbasePro", "GDAX"},
|
||||
} {
|
||||
out, err := new(Version2).DowngradeExchange(context.Background(), []byte(`{"name":"`+tt[0]+`"}`))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, out)
|
||||
assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0])
|
||||
}
|
||||
}
|
||||
198
config/versions/versions.go
Normal file
198
config/versions/versions.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Package versions handles config upgrades and downgrades
|
||||
/*
|
||||
- Versions must be stateful, and not rely upon type definitions in the config pkg
|
||||
|
||||
- Instead versions should localise types into vN/types.go to avoid issues with subsequent changes
|
||||
|
||||
- Versions must upgrade to the next version. Do not retrospectively change versions to match new type changes. Create a new version
|
||||
|
||||
- Versions must implement ExchangeVersion or ConfigVersion, and may implement both
|
||||
*/
|
||||
package versions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingVersion = errors.New("missing version")
|
||||
errVersionIncompatible = errors.New("version does not implement ConfigVersion or ExchangeVersion")
|
||||
errModifyingExchange = errors.New("error modifying exchange config")
|
||||
errNoVersions = errors.New("error retrieving latest config version: No config versions are registered")
|
||||
errApplyingVersion = errors.New("error applying version")
|
||||
)
|
||||
|
||||
// ConfigVersion is a version that affects the general configuration
|
||||
type ConfigVersion interface {
|
||||
UpgradeConfig(context.Context, []byte) ([]byte, error)
|
||||
DowngradeConfig(context.Context, []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// ExchangeVersion is a version that affects specific exchange configurations
|
||||
type ExchangeVersion interface {
|
||||
Exchanges() []string // Use `*` for all exchanges
|
||||
UpgradeExchange(context.Context, []byte) ([]byte, error)
|
||||
DowngradeExchange(context.Context, []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// manager contains versions registerVersioned during import init
|
||||
type manager struct {
|
||||
m sync.RWMutex
|
||||
versions []any
|
||||
}
|
||||
|
||||
// Manager is a public instance of the config version manager
|
||||
var Manager = &manager{}
|
||||
|
||||
// Deploy upgrades or downgrades the config between versions
|
||||
func (m *manager) Deploy(ctx context.Context, j []byte) ([]byte, error) {
|
||||
if err := m.checkVersions(); err != nil {
|
||||
return j, err
|
||||
}
|
||||
|
||||
target, err := m.latest()
|
||||
if err != nil {
|
||||
return j, err
|
||||
}
|
||||
|
||||
m.m.RLock()
|
||||
defer m.m.RUnlock()
|
||||
|
||||
current64, err := jsonparser.GetInt(j, "version")
|
||||
current := int(current64)
|
||||
switch {
|
||||
case errors.Is(err, jsonparser.KeyPathNotFoundError):
|
||||
current = -1
|
||||
case err != nil:
|
||||
return j, fmt.Errorf("%w `version`: %w", common.ErrGettingField, err)
|
||||
case target == current:
|
||||
return j, nil
|
||||
}
|
||||
|
||||
for current != target {
|
||||
next := current + 1
|
||||
action := "upgrade"
|
||||
configMethod := ConfigVersion.UpgradeConfig
|
||||
exchMethod := ExchangeVersion.UpgradeExchange
|
||||
|
||||
if target < current {
|
||||
next = current - 1
|
||||
action = "downgrade"
|
||||
configMethod = ConfigVersion.DowngradeConfig
|
||||
exchMethod = ExchangeVersion.DowngradeExchange
|
||||
}
|
||||
|
||||
log.Printf("Running %s to config version %v\n", action, next)
|
||||
|
||||
patch := m.versions[next]
|
||||
|
||||
if cPatch, ok := patch.(ConfigVersion); ok {
|
||||
if j, err = configMethod(cPatch, ctx, j); err != nil {
|
||||
return j, fmt.Errorf("%w %s to %v: %w", errApplyingVersion, action, next, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ePatch, ok := patch.(ExchangeVersion); ok {
|
||||
if j, err = exchangeDeploy(ctx, ePatch, exchMethod, j); err != nil {
|
||||
return j, fmt.Errorf("%w %s to %v: %w", errApplyingVersion, action, next, err)
|
||||
}
|
||||
}
|
||||
|
||||
current = next
|
||||
|
||||
if j, err = jsonparser.Set(j, []byte(strconv.Itoa(current)), "version"); err != nil {
|
||||
return j, fmt.Errorf("%w `version` during %s to %v: %w", common.ErrSettingField, action, next, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Version management finished")
|
||||
|
||||
return j, nil
|
||||
}
|
||||
|
||||
func exchangeDeploy(ctx context.Context, patch ExchangeVersion, method func(ExchangeVersion, context.Context, []byte) ([]byte, error), j []byte) ([]byte, error) {
|
||||
var errs error
|
||||
wanted := patch.Exchanges()
|
||||
var i int
|
||||
eFunc := func(exchOrig []byte, _ jsonparser.ValueType, _ int, _ error) {
|
||||
defer func() { i++ }()
|
||||
name, err := jsonparser.GetString(exchOrig, "name")
|
||||
if err != nil {
|
||||
errs = common.AppendError(errs, fmt.Errorf("%w: %w `name`: %w", errModifyingExchange, common.ErrGettingField, err))
|
||||
return
|
||||
}
|
||||
for _, want := range wanted {
|
||||
if want != "*" && want != name {
|
||||
continue
|
||||
}
|
||||
exchNew, err := method(patch, ctx, exchOrig)
|
||||
if err != nil {
|
||||
errs = common.AppendError(errs, fmt.Errorf("%w: %w", errModifyingExchange, err))
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(exchNew, exchOrig) {
|
||||
if j, err = jsonparser.Set(j, exchNew, "exchanges", "["+strconv.Itoa(i)+"]"); err != nil {
|
||||
errs = common.AppendError(errs, fmt.Errorf("%w: %w `exchanges.[%d]`: %w", errModifyingExchange, common.ErrSettingField, i, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v, dataType, _, err := jsonparser.Get(j, "exchanges")
|
||||
switch {
|
||||
case errors.Is(err, jsonparser.KeyPathNotFoundError), dataType != jsonparser.Array:
|
||||
return j, nil
|
||||
case err != nil:
|
||||
return j, fmt.Errorf("%w: %w `exchanges`: %w", errModifyingExchange, common.ErrGettingField, err)
|
||||
}
|
||||
if _, err := jsonparser.ArrayEach(bytes.Clone(v), eFunc); err != nil {
|
||||
return j, err
|
||||
}
|
||||
return j, errs
|
||||
}
|
||||
|
||||
// registerVersion takes instances of config versions and adds them to the registry
|
||||
func (m *manager) registerVersion(ver int, v any) {
|
||||
m.m.Lock()
|
||||
defer m.m.Unlock()
|
||||
if ver >= len(m.versions) {
|
||||
m.versions = slices.Grow(m.versions, ver+1)[:ver+1]
|
||||
}
|
||||
m.versions[ver] = v
|
||||
}
|
||||
|
||||
// latest returns the highest version number
|
||||
func (m *manager) latest() (int, error) {
|
||||
m.m.RLock()
|
||||
defer m.m.RUnlock()
|
||||
if len(m.versions) == 0 {
|
||||
return 0, errNoVersions
|
||||
}
|
||||
return len(m.versions) - 1, nil
|
||||
}
|
||||
|
||||
// checkVersions ensures that registered versions are consistent
|
||||
func (m *manager) checkVersions() error {
|
||||
m.m.RLock()
|
||||
defer m.m.RUnlock()
|
||||
for ver, v := range m.versions {
|
||||
switch v.(type) {
|
||||
case ExchangeVersion, ConfigVersion:
|
||||
default:
|
||||
return fmt.Errorf("%w: %v", errVersionIncompatible, ver)
|
||||
}
|
||||
if v == nil {
|
||||
return fmt.Errorf("%w: v%v", errMissingVersion, ver)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
113
config/versions/versions_test.go
Normal file
113
config/versions/versions_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
)
|
||||
|
||||
func TestDeploy(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := manager{}
|
||||
_, err := m.Deploy(context.Background(), []byte(``))
|
||||
assert.ErrorIs(t, err, errNoVersions)
|
||||
|
||||
m.registerVersion(1, &TestVersion1{})
|
||||
_, err = m.Deploy(context.Background(), []byte(``))
|
||||
require.ErrorIs(t, err, errVersionIncompatible)
|
||||
|
||||
m = manager{}
|
||||
|
||||
m.registerVersion(0, &Version0{})
|
||||
_, err = m.Deploy(context.Background(), []byte(`not an object`))
|
||||
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError, "Must throw the correct error trying to add version to bad json")
|
||||
require.ErrorIs(t, err, common.ErrSettingField, "Must throw the correct error trying to add version to bad json")
|
||||
require.ErrorContains(t, err, "version", "Must throw the correct error trying to add version to bad json")
|
||||
|
||||
_, err = m.Deploy(context.Background(), []byte(`{"version":"not an int"}`))
|
||||
require.ErrorIs(t, err, common.ErrGettingField, "Must throw the correct error trying to get version from bad json")
|
||||
|
||||
in := []byte(`{"version":0,"exchanges":[{"name":"Juan"}]}`)
|
||||
j, err := m.Deploy(context.Background(), in)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(in), string(j))
|
||||
|
||||
m.registerVersion(1, &Version1{})
|
||||
j, err = m.Deploy(context.Background(), in)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(j), `"version":1`)
|
||||
|
||||
m.versions = m.versions[:1]
|
||||
j, err = m.Deploy(context.Background(), j)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(j), `"version":0`)
|
||||
|
||||
m.versions = append(m.versions, &TestVersion2{ConfigErr: true, ExchErr: false}) // Bit hacky, but this will actually work
|
||||
_, err = m.Deploy(context.Background(), j)
|
||||
require.ErrorIs(t, err, errUpgrade)
|
||||
|
||||
m.versions[1] = &TestVersion2{ConfigErr: false, ExchErr: true}
|
||||
_, err = m.Deploy(context.Background(), in)
|
||||
require.Implements(t, (*ExchangeVersion)(nil), m.versions[1])
|
||||
require.ErrorIs(t, err, errUpgrade)
|
||||
}
|
||||
|
||||
// TestExchangeDeploy exercises exchangeDeploy
|
||||
// There are a number of error paths we can't currently cover without exposing unacceptable risks to the hot-paths as well
|
||||
func TestExchangeDeploy(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := manager{}
|
||||
_, err := m.Deploy(context.Background(), []byte(``))
|
||||
assert.ErrorIs(t, err, errNoVersions)
|
||||
|
||||
v := &TestVersion2{}
|
||||
in := []byte(`{"version":0,"exchanges":[{}]}`)
|
||||
_, err = exchangeDeploy(context.Background(), v, ExchangeVersion.UpgradeExchange, in)
|
||||
require.ErrorIs(t, err, errModifyingExchange)
|
||||
require.ErrorIs(t, err, common.ErrGettingField)
|
||||
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError)
|
||||
require.ErrorContains(t, err, "`name`")
|
||||
|
||||
in = []byte(`{"version":0,"exchanges":[{"name":"Juan"},{"name":"Megashaft"}]}`)
|
||||
_, err = exchangeDeploy(context.Background(), v, ExchangeVersion.UpgradeExchange, in)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRegisterVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := manager{}
|
||||
|
||||
m.registerVersion(0, &Version0{})
|
||||
assert.NotEmpty(t, m.versions)
|
||||
|
||||
m.registerVersion(2, &TestVersion2{})
|
||||
require.Equal(t, 3, len(m.versions), "Must allocate a space for missing version 1")
|
||||
require.NotNil(t, m.versions[2], "Must put Version 2 in the correct slot")
|
||||
require.Nil(t, m.versions[1], "Must leave Version 1 alone")
|
||||
|
||||
m.registerVersion(1, &TestVersion1{})
|
||||
require.Equal(t, 3, len(m.versions), "Must leave len alone when registering out-of-sequence")
|
||||
require.NotNil(t, m.versions[1], "Must put Version 1 in the correct slot")
|
||||
}
|
||||
|
||||
func TestLatest(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := manager{}
|
||||
_, err := m.latest()
|
||||
require.ErrorIs(t, err, errNoVersions)
|
||||
|
||||
m.registerVersion(0, &Version0{})
|
||||
m.registerVersion(1, &Version1{})
|
||||
v, err := m.latest()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, v)
|
||||
|
||||
m.registerVersion(2, &Version2{})
|
||||
v, err = m.latest()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, v)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
)
|
||||
|
||||
@@ -269,9 +270,16 @@ func TestConfigAllJsonResponse(t *testing.T) {
|
||||
var responseConfig config.Config
|
||||
err = json.Unmarshal(body, &responseConfig)
|
||||
assert.NoError(t, err, "Unmarshal should not error")
|
||||
for _, e := range responseConfig.Exchanges {
|
||||
for i, e := range responseConfig.Exchanges {
|
||||
err = e.CurrencyPairs.SetDelimitersFromConfig()
|
||||
assert.NoError(t, err, "SetDelimitersFromConfig should not error")
|
||||
// Using require here makes it much easier to isolate differences per-exchange than below
|
||||
// We look into pointers separately
|
||||
for a, p := range e.CurrencyPairs.Pairs {
|
||||
require.Equalf(t, c.Exchanges[i].CurrencyPairs.Pairs[a], p, "%s exchange Config CurrencyManager Pairs for asset %s must match api response", e.Name, a)
|
||||
}
|
||||
require.Equalf(t, c.Exchanges[i].CurrencyPairs, e.CurrencyPairs, "%s exchange Config CurrencyManager must match api response", e.Name)
|
||||
require.Equalf(t, c.Exchanges[i], e, "%s exchange Config must match api response", e.Name) // require here makes it much easier to isolate differences than below
|
||||
}
|
||||
assert.Equal(t, c, responseConfig, "Config should match api response")
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func loadConfigWithSettings(settings *Settings, flagSet map[string]bool) (*confi
|
||||
conf := &config.Config{}
|
||||
err = conf.ReadConfigFromFile(filePath, settings.EnableDryRun)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(config.ErrFailureOpeningConfig, filePath, err)
|
||||
return nil, fmt.Errorf("%w %s: %w", config.ErrFailureOpeningConfig, filePath, err)
|
||||
}
|
||||
// Apply overrides from settings
|
||||
if flagSet["datadir"] {
|
||||
|
||||
1
go.mod
1
go.mod
@@ -26,6 +26,7 @@ require (
|
||||
github.com/volatiletech/null v8.0.0+incompatible
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697
|
||||
|
||||
2
go.sum
2
go.sum
@@ -313,6 +313,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
Reference in New Issue
Block a user