Config: AssetEnabled upgrade (#1735)

* Config: Move assetEnabled upgrade to Version management

* Assets: Do not error on asset not enabled, or disabled

This became more messy with Disabling something that's defaulted to
disabled.
Taking an idealogical stance against erroring that what you want to have
done is already done.

* CurrencyManager: Set AssetEnabled when StorePairs(enabled)

* RPCServer: Fix tests expecting StoreAssetPairFormat to enable the asset

Also assertifies

* Bitfinex: Fix tests for MarginFunding subs

* GCTWrapper: Improve TestMain clarity

* BTSE: Add futures to testconfig

* Exchanges: Rename StoreAssetPairStore

Previously we were calling it "Format", but accepting everything from
the PairStore.
We were also defaulting to turning the Asset on.

Now callers need to get their AssetEnabled set as they want it, so
there's no magic

This change also moves responsibility for error wrapping outside to the
caller.

* Config: AssetEnabled upgrade should respect assetTypes

Previously we ignored the field and just turned on everything.
I think that was because we couldn't get at the old value.
In either case, we have the option to do better, and respect the
assetEnabled value

* Config: Improve exchange config version upgrade error messages
This commit is contained in:
Gareth Kirwan
2025-03-17 17:47:37 +07:00
committed by GitHub
parent 2a11c94dc4
commit 16d2d9f35a
41 changed files with 717 additions and 884 deletions

View File

@@ -7,15 +7,12 @@ import (
"strings"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Public errors
var (
ErrAssetAlreadyEnabled = errors.New("asset already enabled")
ErrAssetIsNil = errors.New("asset is nil")
ErrAssetNotFound = errors.New("asset type not found in pair store")
ErrPairAlreadyEnabled = errors.New("pair already enabled")
ErrPairFormatIsNil = errors.New("pair format is nil")
@@ -38,7 +35,7 @@ func (p *PairsManager) GetAssetTypes(enabled bool) asset.Items {
defer p.mutex.RUnlock()
assetTypes := make(asset.Items, 0, len(p.Pairs))
for k, ps := range p.Pairs {
if enabled && (ps.AssetEnabled == nil || !*ps.AssetEnabled) {
if enabled && !ps.AssetEnabled {
continue
}
assetTypes = append(assetTypes, k)
@@ -205,8 +202,11 @@ func (p *PairsManager) GetFormat(a asset.Item, request bool) (PairFormat, error)
return *pFmt, nil
}
// StorePairs stores a list of pairs based on the asset type and whether
// they're enabled or not
// StorePairs stores a list of pairs for an asset type
// If enabled is true:
// * AssetEnabled is set true if the pair list is not empty
// * pairs replace the Enabled pairs
// * pairs are added to Available pairs
func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error {
if !a.IsValid() {
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
@@ -226,6 +226,10 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error
}
if enabled {
if len(pairs) != 0 {
pairStore.AssetEnabled = true
pairStore.Available.Add(pairs...)
}
pairStore.Enabled = slices.Clone(pairs)
} else {
pairStore.Available = slices.Clone(pairs)
@@ -246,9 +250,7 @@ func (p *PairsManager) EnsureOnePairEnabled() (Pair, asset.Item, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
for _, v := range p.Pairs {
if v.AssetEnabled == nil ||
!*v.AssetEnabled ||
len(v.Available) == 0 {
if !v.AssetEnabled || len(v.Available) == 0 {
continue
}
if len(v.Enabled) > 0 {
@@ -256,9 +258,7 @@ func (p *PairsManager) EnsureOnePairEnabled() (Pair, asset.Item, error) {
}
}
for k, v := range p.Pairs {
if v.AssetEnabled == nil ||
!*v.AssetEnabled ||
len(v.Available) == 0 {
if !v.AssetEnabled || len(v.Available) == 0 {
continue
}
rp, err := v.Available.GetRandomPair()
@@ -324,8 +324,7 @@ func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error {
}
if !pairStore.Available.Contains(pair, true) {
return fmt.Errorf("%s %w in the list of available pairs",
pair, ErrPairNotFound)
return fmt.Errorf("%s %w in the list of available pairs", pair, ErrPairNotFound)
}
pairStore.Enabled = pairStore.Enabled.Add(pair)
return nil
@@ -348,10 +347,7 @@ func (p *PairsManager) IsPairAvailable(pair Pair, a asset.Item) (bool, error) {
if err != nil {
return false, err
}
if pairStore.AssetEnabled == nil {
return false, fmt.Errorf("%s %w", a, ErrAssetIsNil)
}
return *pairStore.AssetEnabled && pairStore.Available.Contains(pair, true), nil
return pairStore.AssetEnabled && pairStore.Available.Contains(pair, true), nil
}
// IsPairEnabled checks if a pair is enabled for an enabled asset type
@@ -371,10 +367,7 @@ func (p *PairsManager) IsPairEnabled(pair Pair, a asset.Item) (bool, error) {
if err != nil {
return false, err
}
if pairStore.AssetEnabled == nil {
return false, fmt.Errorf("%s %w", a, ErrAssetIsNil)
}
return *pairStore.AssetEnabled && pairStore.Enabled.Contains(pair, true), nil
return pairStore.AssetEnabled && pairStore.Enabled.Contains(pair, true), nil
}
// IsAssetEnabled checks to see if an asset is enabled
@@ -391,11 +384,7 @@ func (p *PairsManager) IsAssetEnabled(a asset.Item) error {
return err
}
if pairStore.AssetEnabled == nil {
return fmt.Errorf("%s %w", a, ErrAssetIsNil)
}
if !*pairStore.AssetEnabled {
if !pairStore.AssetEnabled {
return fmt.Errorf("%s %w", a, asset.ErrNotEnabled)
}
return nil
@@ -424,18 +413,8 @@ func (p *PairsManager) SetAssetEnabled(a asset.Item, enabled bool) error {
return err
}
if pairStore.AssetEnabled == nil {
pairStore.AssetEnabled = convert.BoolPtr(enabled)
return nil
}
pairStore.AssetEnabled = enabled
if !*pairStore.AssetEnabled && !enabled {
return errors.New("asset already disabled")
} else if *pairStore.AssetEnabled && enabled {
return ErrAssetAlreadyEnabled
}
*pairStore.AssetEnabled = enabled
return nil
}
@@ -547,13 +526,8 @@ func (ps *PairStore) clone() *PairStore {
return nil
}
var assetEnabled *bool
if ps.AssetEnabled != nil {
assetEnabled = convert.BoolPtr(*ps.AssetEnabled)
}
return &PairStore{
AssetEnabled: assetEnabled,
AssetEnabled: ps.AssetEnabled,
Enabled: slices.Clone(ps.Enabled),
Available: slices.Clone(ps.Available),
RequestFormat: ps.RequestFormat.clone(),

View File

@@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
@@ -24,7 +23,7 @@ func initTest(t *testing.T) *PairsManager {
}
spot := &PairStore{
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Available: spotAvailable,
Enabled: spotEnabled,
RequestFormat: &PairFormat{Uppercase: true},
@@ -32,7 +31,7 @@ func initTest(t *testing.T) *PairsManager {
}
futures := &PairStore{
AssetEnabled: convert.BoolPtr(false),
AssetEnabled: false,
Available: spotAvailable,
Enabled: spotEnabled,
RequestFormat: &PairFormat{Uppercase: true},
@@ -463,72 +462,54 @@ func TestEnablePair(t *testing.T) {
}
}
func TestIsAssetEnabled_SetAssetEnabled(t *testing.T) {
func TestAssetEnabled(t *testing.T) {
t.Parallel()
p := initTest(t)
err := p.IsAssetEnabled(asset.Empty)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
assert.ErrorIs(t, err, asset.ErrNotSupported)
p.Pairs = nil
// Test enabling a pair when the pair manager is not initialised
err = p.IsAssetEnabled(asset.Spot)
if err == nil {
t.Error("unexpected result")
}
assert.ErrorIs(t, err, ErrPairManagerNotInitialised)
err = p.SetAssetEnabled(asset.Spot, true)
if err == nil {
t.Fatal("unexpected result")
}
assert.ErrorIs(t, err, ErrPairManagerNotInitialised)
// Test asset type which doesn't exist
p = initTest(t)
p.Pairs[asset.Spot].AssetEnabled = nil
p.Pairs[asset.Spot].AssetEnabled = false
err = p.IsAssetEnabled(asset.Spot)
if err == nil {
t.Error("unexpected result")
}
assert.ErrorIs(t, err, asset.ErrNotEnabled)
err = p.SetAssetEnabled(asset.Spot, false)
if err != nil {
t.Fatal(err)
}
assert.NoError(t, err)
err = p.SetAssetEnabled(asset.Spot, false)
if err == nil {
t.Fatal("unexpected result")
}
assert.NoError(t, err, "Setting to disabled twice should not error")
err = p.IsAssetEnabled(asset.Spot)
if err == nil {
t.Error("unexpected result")
}
assert.ErrorIs(t, err, asset.ErrNotEnabled)
err = p.SetAssetEnabled(asset.Spot, true)
if err != nil {
t.Fatal(err)
}
assert.NoError(t, err)
err = p.SetAssetEnabled(asset.Spot, true)
if err == nil {
t.Fatal("unexpected result")
}
assert.NoError(t, err, "Setting to enabled twice should not error")
err = p.IsAssetEnabled(asset.Spot)
if err != nil {
t.Error("unexpected result")
}
assert.NoError(t, err, "IsAssetEnabled should not error")
}
// TestFullStoreUnmarshalMarshal tests json Mashal and Unmarshal
func TestFullStoreUnmarshalMarshal(t *testing.T) {
t.Parallel()
var um = make(FullStore)
um[asset.Spot] = &PairStore{AssetEnabled: convert.BoolPtr(true)}
um[asset.Spot] = &PairStore{AssetEnabled: true}
data, err := json.Marshal(um)
if err != nil {
@@ -591,7 +572,7 @@ func TestIsPairAvailable(t *testing.T) {
pm.Pairs[asset.PerpetualSwap] = &PairStore{}
_, err = pm.IsPairAvailable(cp, asset.PerpetualSwap)
assert.ErrorIs(t, err, ErrAssetIsNil, "Should error when store AssetEnabled is nil")
require.NoError(t, err, "Must not error when store is empty")
_, err = pm.IsPairAvailable(EMPTYPAIR, asset.PerpetualSwap)
assert.ErrorIs(t, err, ErrCurrencyPairEmpty, "Should error when currency pair is empty")
@@ -602,62 +583,35 @@ func TestIsPairEnabled(t *testing.T) {
pm := initTest(t)
cp := NewPairWithDelimiter("BTC", "USD", "-")
enabled, err := pm.IsPairEnabled(cp, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if !enabled {
t.Fatal("expected pair to be enabled")
}
require.NoError(t, err)
assert.True(t, enabled, "IsPairEnabled should return true when pair is enabled")
enabled, err = pm.IsPairEnabled(NewPair(SAFE, MOONRISE), asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if enabled {
t.Fatal("expected pair to be disabled")
}
require.NoError(t, err)
assert.False(t, enabled, "IsPairEnabled should return false when pair does not exist")
enabled, err = pm.IsPairEnabled(cp, asset.Futures)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if enabled {
t.Fatal("expected pair to be disabled because asset type is not enabled")
}
require.NoError(t, err)
assert.False(t, enabled, "IsPairEnabled should return false when asset not enabled")
cp = NewPairWithDelimiter("XRP", "DOGE", "-")
enabled, err = pm.IsPairEnabled(cp, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if enabled {
t.Fatal("expected pair to be disabled because pair not found in enabled list")
}
require.NoError(t, err)
assert.False(t, enabled, "IsPairEnabled should return false when pair not in enabled list")
_, err = pm.IsPairEnabled(cp, asset.PerpetualSwap)
if !errors.Is(err, ErrAssetNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrAssetNotFound)
}
assert.ErrorIs(t, err, ErrAssetNotFound)
_, err = pm.IsPairEnabled(cp, asset.Item(1337))
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
assert.ErrorIs(t, err, asset.ErrNotSupported)
pm.Pairs[asset.PerpetualSwap] = &PairStore{}
_, err = pm.IsPairEnabled(cp, asset.PerpetualSwap)
if !errors.Is(err, ErrAssetIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrAssetIsNil)
}
enabled, err = pm.IsPairEnabled(cp, asset.PerpetualSwap)
require.NoError(t, err, "Must not error when store is empty")
assert.False(t, enabled, "Should return false when store is empty")
_, err = pm.IsPairEnabled(EMPTYPAIR, asset.PerpetualSwap)
if !errors.Is(err, ErrCurrencyPairEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty)
}
assert.ErrorIs(t, err, ErrCurrencyPairEmpty)
}
func TestEnsureOnePairEnabled(t *testing.T) {
@@ -667,7 +621,7 @@ func TestEnsureOnePairEnabled(t *testing.T) {
Pairs: map[asset.Item]*PairStore{
asset.Futures: {},
asset.Spot: {
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Available: []Pair{
p,
},
@@ -700,13 +654,13 @@ func TestEnsureOnePairEnabled(t *testing.T) {
pm = PairsManager{
Pairs: map[asset.Item]*PairStore{
asset.Futures: {
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Available: []Pair{
NewPair(BTC, USDC),
},
},
asset.Spot: {
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Enabled: []Pair{
p,
},
@@ -733,11 +687,11 @@ func TestEnsureOnePairEnabled(t *testing.T) {
pm = PairsManager{
Pairs: map[asset.Item]*PairStore{
asset.Futures: {
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Available: []Pair{p},
},
asset.Options: {
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Available: []Pair{},
},
},
@@ -776,20 +730,20 @@ func TestLoad(t *testing.T) {
RequestFormat: fmt2,
Pairs: map[asset.Item]*PairStore{
asset.Futures: {
AssetEnabled: convert.BoolPtr(true),
AssetEnabled: true,
Available: []Pair{p},
},
asset.Options: {
AssetEnabled: convert.BoolPtr(false),
AssetEnabled: false,
Available: []Pair{},
},
},
}
base.Load(&seed)
assert.True(t, *base.Pairs[asset.Futures].AssetEnabled, "Futures AssetEnabled should be true")
assert.True(t, base.Pairs[asset.Futures].AssetEnabled, "Futures AssetEnabled should be true")
assert.True(t, base.Pairs[asset.Futures].Available.Contains(p, true), "Futures Available Pairs should contain BTCUSDT")
assert.False(t, *base.Pairs[asset.Options].AssetEnabled, "Options AssetEnabled should be false")
assert.False(t, base.Pairs[asset.Options].AssetEnabled, "Options AssetEnabled should be false")
assert.Equal(t, tt, base.LastUpdated, "Last Updated should be correct")
assert.Equal(t, fmt1.Uppercase, base.ConfigFormat.Uppercase, "ConfigFormat Uppercase should be correct")
assert.Equal(t, fmt2.Delimiter, base.RequestFormat.Delimiter, "RequestFormat Delimiter should be correct")
@@ -909,7 +863,7 @@ func TestIsAssetSupported(t *testing.T) {
p := PairsManager{
Pairs: FullStore{
asset.Spot: {
AssetEnabled: convert.BoolPtr(false),
AssetEnabled: false,
},
},
}

View File

@@ -24,7 +24,7 @@ type FullStore map[asset.Item]*PairStore
// PairStore stores a currency pair store
type PairStore struct {
AssetEnabled *bool `json:"assetEnabled"`
AssetEnabled bool `json:"assetEnabled"`
Enabled Pairs `json:"enabled"`
Available Pairs `json:"available"`
RequestFormat *PairFormat `json:"requestFormat,omitempty"`