Engine: Protocol Features, coverage, types, BTC markets websocket (#368)

* Attempts to update orderbook so it doesn't need to sort

* Reverts the ws ob stuff. Gets rid of sorting because it happens later. Adds some exchange features

* update existing feature lists. Expands list definition to match my emotions

* Adds bithumb bitmex and bitstamp. adds a couple more types

* Features for you, features for me, features for bittrex, btcmarkets, btse, coinbasepro, coinut, exmo, gateio and gemini

* Features for hitbtc, huobi, itbit, kraken, lakebtc, lbank, localbitcoins, okcoin, okex, poloniex, yobit, zb

* Who can forget good old alphapoint?

* Adds btcmarksets websocket :glitch_crab: fixes alphapoint features

* Adds extra data not in the documentation :/

* Replaces websocket features by using protocol features. However, it breaks it due to import cycles. I'm not sure what I'll do just yet

* Removes import cycle via duplicate structs.

* Increases coverage of config with `TestCheckCurrencyConfigValues`. Moves all currency pair package types into their own files or places it at the bottom of files if necessary

* Increase coverage in code.go

* One way of determining a test has failed, is when to it fails. Removed redundant explanation

* Increases code coverage of conversion

* Lint fixes

* Fixes orderbook tests

* Re-adds sorting because its important to still have the internal pre-processed orderbook to be representative of a real orderbook

* Secret lints that did not show up via Windows linting

* Adds protocol package to contain exchange features

* Fixes protocol implementation

* Fixes ws tests

* Addresses the following: Removes st-st-stutters in config types, changes GetAvailableForexProviders -> GetSupportedForexProviders, removes errors from tests where error is nil, removes orderbook setup when not necessary, removes import newlines, removes false bools from declaration, changes should of to should have

* imports and casing

* Fixes two more nil error checks
This commit is contained in:
Scott
2019-10-22 10:56:20 +11:00
committed by Adrian Gallagher
parent ec0ed1c1e5
commit ccfcdf26aa
156 changed files with 5228 additions and 4337 deletions

View File

@@ -13,7 +13,6 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -27,67 +26,6 @@ import (
log "github.com/thrasher-corp/gocryptotrader/logger"
)
// Constants declared here are filename strings and test strings
const (
FXProviderFixer = "fixer"
EncryptedConfigFile = "config.dat"
ConfigFile = "config.json"
ConfigTestFile = "../testdata/configtest.json"
configFileEncryptionPrompt = 0
configFileEncryptionEnabled = 1
configFileEncryptionDisabled = -1
configPairsLastUpdatedWarningThreshold = 30 // 30 days
configDefaultHTTPTimeout = time.Second * 15
configDefaultWebsocketResponseCheckTimeout = time.Millisecond * 30
configDefaultWebsocketResponseMaxLimit = time.Second * 7
configDefaultWebsocketOrderbookBufferLimit = 5
configDefaultWebsocketTrafficTimeout = time.Second * 30
configMaxAuthFailures = 3
defaultNTPAllowedDifference = 50000000
defaultNTPAllowedNegativeDifference = 50000000
DefaultAPIKey = "Key"
DefaultAPISecret = "Secret"
DefaultAPIClientID = "ClientID"
)
// Constants here hold some messages
const (
ErrExchangeNameEmpty = "exchange #%d name is empty"
ErrExchangeAvailablePairsEmpty = "exchange %s available pairs is empty"
ErrExchangeEnabledPairsEmpty = "exchange %s enabled pairs is empty"
ErrExchangeBaseCurrenciesEmpty = "exchange %s base currencies is empty"
ErrExchangeNotFound = "exchange %s not found"
ErrNoEnabledExchanges = "no exchanges enabled"
ErrCryptocurrenciesEmpty = "cryptocurrencies variable is empty"
ErrFailureOpeningConfig = "fatal error opening %s file. Error: %s"
ErrCheckingConfigValues = "fatal error checking config values. Error: %s"
ErrSavingConfigBytesMismatch = "config file %q bytes comparison doesn't match, read %s expected %s"
WarningWebserverCredentialValuesEmpty = "webserver support disabled due to empty Username/Password values"
WarningWebserverListenAddressInvalid = "webserver support disabled due to invalid listen address"
WarningExchangeAuthAPIDefaultOrEmptyValues = "exchange %s authenticated API support disabled due to default/empty APIKey/Secret/ClientID values"
WarningPairsLastUpdatedThresholdExceeded = "exchange %s last manual update of available currency pairs has exceeded %d days. Manual update required!"
)
// Constants here define unset default values displayed in the config.json
// file
const (
APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API"
WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API"
DefaultUnsetAPIKey = "Key"
DefaultUnsetAPISecret = "Secret"
DefaultUnsetAccountPlan = "accountPlan"
DefaultForexProviderExchangeRatesAPI = "ExchangeRates"
)
// Variables here are used for configuration
var (
Cfg Config
IsInitialSetup bool
testBypass bool
m sync.Mutex
)
// GetCurrencyConfig returns currency configurations
func (c *Config) GetCurrencyConfig() CurrencyConfig {
return c.Currency
@@ -982,14 +920,14 @@ func (c *Config) CheckExchangeConfigValues() error {
}
if !c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates && !c.Exchanges[i].Features.Supports.WebsocketCapabilities.AutoPairUpdates {
lastUpdated := convert.UnixTimestampToTime(c.Exchanges[i].CurrencyPairs.LastUpdated)
lastUpdated = lastUpdated.AddDate(0, 0, configPairsLastUpdatedWarningThreshold)
lastUpdated = lastUpdated.AddDate(0, 0, pairsLastUpdatedWarningThreshold)
if lastUpdated.Unix() <= time.Now().Unix() {
log.Warnf(log.ExchangeSys, WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, configPairsLastUpdatedWarningThreshold)
log.Warnf(log.ExchangeSys, WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, pairsLastUpdatedWarningThreshold)
}
}
if c.Exchanges[i].HTTPTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s HTTP Timeout value not set, defaulting to %v.\n", c.Exchanges[i].Name, configDefaultHTTPTimeout)
c.Exchanges[i].HTTPTimeout = configDefaultHTTPTimeout
log.Warnf(log.ExchangeSys, "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].HTTPRateLimiter != nil {
@@ -1016,24 +954,24 @@ func (c *Config) CheckExchangeConfigValues() error {
if c.Exchanges[i].WebsocketResponseCheckTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response check timeout value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketResponseCheckTimeout)
c.Exchanges[i].WebsocketResponseCheckTimeout = configDefaultWebsocketResponseCheckTimeout
c.Exchanges[i].Name, defaultWebsocketResponseCheckTimeout)
c.Exchanges[i].WebsocketResponseCheckTimeout = defaultWebsocketResponseCheckTimeout
}
if c.Exchanges[i].WebsocketResponseMaxLimit <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response max limit value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketResponseMaxLimit)
c.Exchanges[i].WebsocketResponseMaxLimit = configDefaultWebsocketResponseMaxLimit
c.Exchanges[i].Name, defaultWebsocketResponseMaxLimit)
c.Exchanges[i].WebsocketResponseMaxLimit = defaultWebsocketResponseMaxLimit
}
if c.Exchanges[i].WebsocketTrafficTimeout <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket response traffic timeout value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketTrafficTimeout)
c.Exchanges[i].WebsocketTrafficTimeout = configDefaultWebsocketTrafficTimeout
c.Exchanges[i].Name, defaultWebsocketTrafficTimeout)
c.Exchanges[i].WebsocketTrafficTimeout = defaultWebsocketTrafficTimeout
}
if c.Exchanges[i].WebsocketOrderbookBufferLimit <= 0 {
log.Warnf(log.ExchangeSys, "Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.",
c.Exchanges[i].Name, configDefaultWebsocketOrderbookBufferLimit)
c.Exchanges[i].WebsocketOrderbookBufferLimit = configDefaultWebsocketOrderbookBufferLimit
c.Exchanges[i].Name, defaultWebsocketOrderbookBufferLimit)
c.Exchanges[i].WebsocketOrderbookBufferLimit = defaultWebsocketOrderbookBufferLimit
}
err := c.CheckPairConsistency(c.Exchanges[i].Name)
if err != nil {
@@ -1065,10 +1003,7 @@ func (c *Config) CheckExchangeConfigValues() error {
// CheckCurrencyConfigValues checks to see if the currency config values are correct or not
func (c *Config) CheckCurrencyConfigValues() error {
fxProviders := forexprovider.GetAvailableForexProviders()
if len(fxProviders) == 0 {
return errors.New("no forex providers available")
}
fxProviders := forexprovider.GetSupportedForexProviders()
if len(fxProviders) != len(c.Currency.ForexProviders) {
for x := range fxProviders {
@@ -1088,27 +1023,25 @@ func (c *Config) CheckCurrencyConfigValues() error {
count := 0
for i := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[i].Enabled {
if c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey && c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
if c.Currency.ForexProviders[i].Name == "CurrencyConverter" &&
c.Currency.ForexProviders[i].PrimaryProvider &&
(c.Currency.ForexProviders[i].APIKey == "" ||
c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey) {
log.Warnln(log.Global, "CurrencyConverter forex provider no longer supports unset API key requests. Switching to ExchangeRates FX provider..")
c.Currency.ForexProviders[i].Enabled = false
c.Currency.ForexProviders[i].PrimaryProvider = false
c.Currency.ForexProviders[i].APIKey = DefaultUnsetAPIKey
c.Currency.ForexProviders[i].APIKeyLvl = -1
continue
}
if c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey &&
c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
log.Warnf(log.Global, "%s enabled forex provider API key not set. Please set this in your config.json file\n", c.Currency.ForexProviders[i].Name)
c.Currency.ForexProviders[i].Enabled = false
c.Currency.ForexProviders[i].PrimaryProvider = false
continue
}
if c.Currency.ForexProviders[i].Name == "CurrencyConverter" {
if c.Currency.ForexProviders[i].Enabled &&
c.Currency.ForexProviders[i].PrimaryProvider &&
(c.Currency.ForexProviders[i].APIKey == "" ||
c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey) {
log.Warnln(log.Global, "CurrencyConverter forex provider no longer supports unset API key requests. Switching to ExchangeRates FX provider..")
c.Currency.ForexProviders[i].Enabled = false
c.Currency.ForexProviders[i].PrimaryProvider = false
c.Currency.ForexProviders[i].APIKey = DefaultUnsetAPIKey
c.Currency.ForexProviders[i].APIKeyLvl = -1
continue
}
}
if c.Currency.ForexProviders[i].APIKeyLvl == -1 && c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
log.Warnf(log.Global, "%s APIKey Level not set, functions limited. Please set this in your config.json file\n",
c.Currency.ForexProviders[i].Name)
@@ -1406,7 +1339,7 @@ func GetFilePath(file string) (string, error) {
}
if flag.Lookup("test.v") != nil && !testBypass {
return ConfigTestFile, nil
return TestFile, nil
}
exePath, err := common.GetExecutablePath()
@@ -1415,8 +1348,8 @@ func GetFilePath(file string) (string, error) {
}
oldDirs := []string{
filepath.Join(exePath, ConfigFile),
filepath.Join(exePath, EncryptedConfigFile),
filepath.Join(exePath, File),
filepath.Join(exePath, EncryptedFile),
}
newDir := common.GetDefaultDataDir(runtime.GOOS)
@@ -1425,8 +1358,8 @@ func GetFilePath(file string) (string, error) {
return "", err
}
newDirs := []string{
filepath.Join(newDir, ConfigFile),
filepath.Join(newDir, EncryptedConfigFile),
filepath.Join(newDir, File),
filepath.Join(newDir, EncryptedFile),
}
// First upgrade the old dir config file if it exists to the corresponding
@@ -1522,23 +1455,23 @@ func (c *Config) ReadConfig(configPath string, dryrun bool) error {
return err
}
if c.EncryptConfig == configFileEncryptionDisabled {
if c.EncryptConfig == fileEncryptionDisabled {
return nil
}
if c.EncryptConfig == configFileEncryptionPrompt {
if c.EncryptConfig == fileEncryptionPrompt {
m.Lock()
IsInitialSetup = true
m.Unlock()
if c.PromptForConfigEncryption(configPath, dryrun) {
c.EncryptConfig = configFileEncryptionEnabled
c.EncryptConfig = fileEncryptionEnabled
return c.SaveConfig(defaultPath, dryrun)
}
}
} else {
errCounter := 0
for {
if errCounter >= configMaxAuthFailures {
if errCounter >= maxAuthFailures {
return errors.New("failed to decrypt config after 3 attempts")
}
key, err := PromptForConfigKey(IsInitialSetup)
@@ -1559,7 +1492,7 @@ func (c *Config) ReadConfig(configPath string, dryrun bool) error {
err = ConfirmConfigJSON(data, &c)
if err != nil {
if errCounter < configMaxAuthFailures {
if errCounter < maxAuthFailures {
log.Error(log.ConfigMgr, "Invalid password.")
}
errCounter++
@@ -1587,7 +1520,7 @@ func (c *Config) SaveConfig(configPath string, dryrun bool) error {
return err
}
if c.EncryptConfig == configFileEncryptionEnabled {
if c.EncryptConfig == fileEncryptionEnabled {
var key []byte
if IsInitialSetup {
@@ -1678,8 +1611,8 @@ func (c *Config) CheckConfig() error {
}
if c.GlobalHTTPTimeout <= 0 {
log.Warnf(log.ConfigMgr, "Global HTTP Timeout value not set, defaulting to %v.\n", configDefaultHTTPTimeout)
c.GlobalHTTPTimeout = configDefaultHTTPTimeout
log.Warnf(log.ConfigMgr, "Global HTTP Timeout value not set, defaulting to %v.\n", defaultHTTPTimeout)
c.GlobalHTTPTimeout = defaultHTTPTimeout
}
if c.NTPClient.Level != 0 {

View File

@@ -43,7 +43,7 @@ func (c *Config) PromptForConfigEncryption(configPath string, dryrun bool) bool
}
if !common.YesOrNo(input) {
c.EncryptConfig = configFileEncryptionDisabled
c.EncryptConfig = fileEncryptionDisabled
err := c.SaveConfig(configPath, dryrun)
if err != nil {
log.Errorf(log.ConfigMgr, "cannot save config %s", err)

View File

@@ -9,7 +9,7 @@ func TestPromptForConfigEncryption(t *testing.T) {
t.Parallel()
if Cfg.PromptForConfigEncryption("", true) {
t.Error("Test failed. PromptForConfigEncryption return incorrect bool")
t.Error("PromptForConfigEncryption return incorrect bool")
}
}
@@ -18,25 +18,25 @@ func TestPromptForConfigKey(t *testing.T) {
byteyBite, err := PromptForConfigKey(true)
if err == nil && len(byteyBite) > 1 {
t.Errorf("Test failed. PromptForConfigKey: %s", err)
t.Errorf("PromptForConfigKey: %s", err)
}
_, err = PromptForConfigKey(false)
if err == nil {
t.Fatal(err)
t.Error("Expected error")
}
}
func TestEncryptConfigFile(t *testing.T) {
_, err := EncryptConfigFile([]byte("test"), nil)
if err == nil {
t.Fatal("Test failed. Expected different result")
t.Fatal("Expected error")
}
sessionDK = []byte("a")
_, err = EncryptConfigFile([]byte("test"), nil)
if err == nil {
t.Fatal("Test failed. Expected different result")
t.Fatal("Expected error")
}
sessionDK, err = makeNewSessionDK([]byte("asdf"))
@@ -60,17 +60,17 @@ func TestDecryptConfigFile(t *testing.T) {
_, err = DecryptConfigFile(result, nil)
if err == nil {
t.Fatal("Test failed. Expected different result")
t.Fatal("Expected error")
}
_, err = DecryptConfigFile([]byte("test"), nil)
if err == nil {
t.Fatal("Test failed. Expected different result")
t.Fatal("Expected error")
}
_, err = DecryptConfigFile([]byte("test"), []byte("AAAAAAAAAAAAAAAA"))
if err == nil {
t.Fatalf("Test failed. Expected %s", errAESBlockSize)
t.Fatalf("Expected %s", errAESBlockSize)
}
result, err = EncryptConfigFile([]byte("test"), []byte("key"))
@@ -86,14 +86,14 @@ func TestDecryptConfigFile(t *testing.T) {
func TestConfirmConfigJSON(t *testing.T) {
var result interface{}
testConfirmJSON, err := ioutil.ReadFile(ConfigTestFile)
testConfirmJSON, err := ioutil.ReadFile(TestFile)
if err != nil {
t.Errorf("Test failed. testConfirmJSON: %s", err)
t.Errorf("testConfirmJSON: %s", err)
}
err = ConfirmConfigJSON(testConfirmJSON, &result)
if err != nil || result == nil {
t.Errorf("Test failed. testConfirmJSON: %s", err)
t.Errorf("testConfirmJSON: %s", err)
}
}
@@ -102,7 +102,7 @@ func TestConfirmECS(t *testing.T) {
ECStest := []byte(EncryptConfirmString)
if !ConfirmECS(ECStest) {
t.Errorf("Test failed. TestConfirmECS: Error finding ECS.")
t.Errorf("TestConfirmECS: Error finding ECS.")
}
}
@@ -113,7 +113,7 @@ func TestRemoveECS(t *testing.T) {
isremoved := RemoveECS(ECStest)
if string(isremoved) != "" {
t.Errorf("Test failed. TestConfirmECS: Error ECS not deleted.")
t.Errorf("TestConfirmECS: Error ECS not deleted.")
}
}
@@ -122,6 +122,6 @@ func TestMakeNewSessionDK(t *testing.T) {
_, err := makeNewSessionDK(nil)
if err == nil {
t.Fatal("Test failed. makeNewSessionDK passed with nil key")
t.Fatal("makeNewSessionDK passed with nil key")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,77 @@ package config
import (
"fmt"
"strings"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
log "github.com/thrasher-corp/gocryptotrader/logger"
"github.com/thrasher-corp/gocryptotrader/portfolio"
)
// Constants declared here are filename strings and test strings
const (
FXProviderFixer = "fixer"
EncryptedFile = "config.dat"
File = "config.json"
TestFile = "../testdata/configtest.json"
fileEncryptionPrompt = 0
fileEncryptionEnabled = 1
fileEncryptionDisabled = -1
pairsLastUpdatedWarningThreshold = 30 // 30 days
defaultHTTPTimeout = time.Second * 15
defaultWebsocketResponseCheckTimeout = time.Millisecond * 30
defaultWebsocketResponseMaxLimit = time.Second * 7
defaultWebsocketOrderbookBufferLimit = 5
defaultWebsocketTrafficTimeout = time.Second * 30
maxAuthFailures = 3
defaultNTPAllowedDifference = 50000000
defaultNTPAllowedNegativeDifference = 50000000
DefaultAPIKey = "Key"
DefaultAPISecret = "Secret"
DefaultAPIClientID = "ClientID"
)
// Constants here hold some messages
const (
ErrExchangeNameEmpty = "exchange #%d name is empty"
ErrExchangeAvailablePairsEmpty = "exchange %s available pairs is empty"
ErrExchangeEnabledPairsEmpty = "exchange %s enabled pairs is empty"
ErrExchangeBaseCurrenciesEmpty = "exchange %s base currencies is empty"
ErrExchangeNotFound = "exchange %s not found"
ErrNoEnabledExchanges = "no exchanges enabled"
ErrCryptocurrenciesEmpty = "cryptocurrencies variable is empty"
ErrFailureOpeningConfig = "fatal error opening %s file. Error: %s"
ErrCheckingConfigValues = "fatal error checking config values. Error: %s"
ErrSavingConfigBytesMismatch = "config file %q bytes comparison doesn't match, read %s expected %s"
WarningWebserverCredentialValuesEmpty = "webserver support disabled due to empty Username/Password values"
WarningWebserverListenAddressInvalid = "webserver support disabled due to invalid listen address"
WarningExchangeAuthAPIDefaultOrEmptyValues = "exchange %s authenticated API support disabled due to default/empty APIKey/Secret/ClientID values"
WarningPairsLastUpdatedThresholdExceeded = "exchange %s last manual update of available currency pairs has exceeded %d days. Manual update required!"
)
// Constants here define unset default values displayed in the config.json
// file
const (
APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API"
WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API"
DefaultUnsetAPIKey = "Key"
DefaultUnsetAPISecret = "Secret"
DefaultUnsetAccountPlan = "accountPlan"
DefaultForexProviderExchangeRatesAPI = "ExchangeRates"
)
// Variables here are used for configuration
var (
Cfg Config
IsInitialSetup bool
testBypass bool
m sync.Mutex
)
// Config is the overarching object that holds all the information for
// prestart management of Portfolio, Communications, Webserver and Enabled
// Exchanges
@@ -313,39 +375,12 @@ type TelegramConfig struct {
VerificationToken string `json:"verificationToken"`
}
// ProtocolFeaturesConfig holds all variables for the exchanges supported features
// for a protocol (e.g REST or Websocket)
type ProtocolFeaturesConfig struct {
TickerBatching bool `json:"tickerBatching,omitempty"`
AutoPairUpdates bool `json:"autoPairUpdates,omitempty"`
AccountBalance bool `json:"accountBalance,omitempty"`
CryptoDeposit bool `json:"cryptoDeposit,omitempty"`
CryptoWithdrawal uint32 `json:"cryptoWithdrawal,omitempty"`
FiatWithdraw bool `json:"fiatWithdraw,omitempty"`
GetOrder bool `json:"getOrder,omitempty"`
GetOrders bool `json:"getOrders,omitempty"`
CancelOrders bool `json:"cancelOrders,omitempty"`
CancelOrder bool `json:"cancelOrder,omitempty"`
SubmitOrder bool `json:"submitOrder,omitempty"`
SubmitOrders bool `json:"submitOrders,omitempty"`
ModifyOrder bool `json:"modifyOrder,omitempty"`
DepositHistory bool `json:"depositHistory,omitempty"`
WithdrawalHistory bool `json:"withdrawalHistory,omitempty"`
TradeHistory bool `json:"tradeHistory,omitempty"`
UserTradeHistory bool `json:"userTradeHistory,omitempty"`
TradeFee bool `json:"tradeFee,omitempty"`
FiatDepositFee bool `json:"fiatDepositFee,omitempty"`
FiatWithdrawalFee bool `json:"fiatWithdrawalFee,omitempty"`
CryptoDepositFee bool `json:"cryptoDepositFee,omitempty"`
CryptoWithdrawalFee bool `json:"cryptoWithdrawalFee,omitempty"`
}
// FeaturesSupportedConfig stores the exchanges supported features
type FeaturesSupportedConfig struct {
REST bool `json:"restAPI"`
RESTCapabilities ProtocolFeaturesConfig `json:"restCapabilities,omitempty"`
Websocket bool `json:"websocketAPI"`
WebsocketCapabilities ProtocolFeaturesConfig `json:"websocketCapabilities,omitempty"`
REST bool `json:"restAPI"`
RESTCapabilities protocol.Features `json:"restCapabilities,omitempty"`
Websocket bool `json:"websocketAPI"`
WebsocketCapabilities protocol.Features `json:"websocketCapabilities,omitempty"`
}
// FeaturesEnabledConfig stores the exchanges enabled features