accounts: Move to instance methods, fix races and isolate tests (#1923)

* Bybit: Fix race in TestUpdateAccountInfo and  TestWSHandleData

* DriveBy rename TestWSHandleData
* This doesn't address running with -race=2+ due to the singleton

* Accounts: Add account.GetService()

* exchange: Assertify TestSetupDefaults

* Exchanges: Add account.Service override for testing

* Exchanges: Remove duplicate IsWebsocketEnabled test from TestSetupDefaults

* Dispatch: Replace nil checks with NilGuard

* Engine: Remove deprecated printAccountHoldingsChangeSummary

* Dispatcher: Add EnsureRunning method

* Accounts: Move singleton accounts service to exchange Accounts

* Move singleton accounts service to exchange Accounts

This maintains the concept of a global store, whilst allowing exchanges
to override it when needed, particularly for testing.

APIServer:

* Remove getAllActiveAccounts from apiserver

Deprecated apiserver only thing using this, so remove it instead of
updating it

* Update comment for UpdateAccountBalances everywhere

* Docs: Add punctuation to function comments

* Bybit: Coverage for wsProcessWalletPushData Save
This commit is contained in:
Gareth Kirwan
2025-10-28 09:52:45 +07:00
committed by GitHub
parent bda9bbec66
commit 73e200e4e7
140 changed files with 3515 additions and 4025 deletions

View File

@@ -19,10 +19,10 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
@@ -2778,44 +2778,49 @@ func TestGetBrokerEarning(t *testing.T) {
}
}
func TestUpdateAccountInfo(t *testing.T) {
func TestUpdateAccountBalances(t *testing.T) {
t.Parallel()
if !mockTests {
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
}
r, err := e.UpdateAccountInfo(t.Context(), asset.Spot)
require.NoError(t, err, "UpdateAccountInfo must not error")
require.NotEmpty(t, r, "UpdateAccountInfo must return account info")
e := testInstance() //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
subAccts, err := e.UpdateAccountBalances(t.Context(), asset.Spot)
require.NoError(t, err, "UpdateAccountBalances must not error")
require.NotEmpty(t, subAccts, "UpdateAccountBalances must return account info")
if mockTests {
require.Len(t, r.Accounts, 1, "Accounts must have 1 item")
require.Len(t, r.Accounts[0].Currencies, 3, "Accounts currencies must have 3 currency items")
require.Len(t, subAccts, 1, "Accounts must have 1 item")
require.Len(t, subAccts[0].Balances, 3, "Accounts currencies must have 3 currency items")
for x := range r.Accounts[0].Currencies {
switch x {
case 0:
assert.Equal(t, currency.USDC, r.Accounts[0].Currencies[x].Currency, "Currency should be USDC")
assert.Equal(t, -30723.63021638, r.Accounts[0].Currencies[x].Total, "Total amount should be correct")
assert.Zero(t, r.Accounts[0].Currencies[x].Hold, "Hold amount should be zero")
assert.Equal(t, 30723.630216383711792744, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be correct")
assert.Zero(t, r.Accounts[0].Currencies[x].Free, "Free amount should be zero")
assert.Zero(t, r.Accounts[0].Currencies[x].AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be zero")
case 1:
assert.Equal(t, currency.AVAX, r.Accounts[0].Currencies[x].Currency, "Currency should be AVAX")
assert.Equal(t, 2473.9, r.Accounts[0].Currencies[x].Total, "Total amount should be correct")
assert.Zero(t, r.Accounts[0].Currencies[x].Hold, "Hold amount should be zero")
assert.Zero(t, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be zero")
assert.Equal(t, 2473.9, r.Accounts[0].Currencies[x].Free, "Free amount should be correct")
assert.Equal(t, 1005.79191187, r.Accounts[0].Currencies[x].AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct")
case 2:
assert.Equal(t, currency.USDT, r.Accounts[0].Currencies[x].Currency, "Currency should be USDT")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Total, "Total amount should be correct")
assert.Zero(t, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be zero")
assert.Zero(t, r.Accounts[0].Currencies[x].Hold, "Hold amount should be zero")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Free, "Free amount should be correct")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct")
}
for _, curr := range []currency.Code{currency.USDC, currency.AVAX, currency.USDT} {
t.Run(curr.String(), func(t *testing.T) {
t.Parallel()
require.Contains(t, subAccts[0].Balances, curr, "Balances must contain currency")
bal := subAccts[0].Balances[curr]
assert.Equal(t, curr, bal.Currency, "Balance Currency should be set")
switch curr {
case currency.USDC:
assert.Equal(t, -30723.63021638, bal.Total, "Total amount should be correct")
assert.Zero(t, bal.Hold, "Hold amount should be zero")
assert.Equal(t, 30723.630216383711792744, bal.Borrowed, "Borrowed amount should be correct")
assert.Zero(t, bal.Free, "Free amount should be zero")
assert.Zero(t, bal.AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be zero")
case currency.AVAX:
assert.Equal(t, 2473.9, bal.Total, "Total amount should be correct")
assert.Zero(t, bal.Hold, "Hold amount should be zero")
assert.Zero(t, bal.Borrowed, "Borrowed amount should be zero")
assert.Equal(t, 2473.9, bal.Free, "Free amount should be correct")
assert.Equal(t, 1005.79191187, bal.AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct")
case currency.USDT:
assert.Equal(t, 935.1415, bal.Total, "Total amount should be correct")
assert.Zero(t, bal.Borrowed, "Borrowed amount should be zero")
assert.Zero(t, bal.Hold, "Hold amount should be zero")
assert.Equal(t, 935.1415, bal.Free, "Free amount should be correct")
assert.Equal(t, 935.1415, bal.AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct")
}
})
}
}
}
@@ -2980,9 +2985,11 @@ var pushDataMap = map[string]string{
"unhandled": `{"topic": "unhandled"}`,
}
func TestPushDataPublic(t *testing.T) {
func TestWSHandleData(t *testing.T) {
t.Parallel()
e := testInstance() //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
keys := slices.Collect(maps.Keys(pushDataMap))
slices.Sort(keys)
for x := range keys {
@@ -3009,14 +3016,21 @@ func TestWSHandleAuthenticatedData(t *testing.T) {
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
e.SetCredentials("test", "test", "", "", "", "")
testexch.FixtureToDataHandler(t, "testdata/wsAuth.json", func(ctx context.Context, r []byte) error {
fErrs := testexch.FixtureToDataHandlerWithErrors(t, "testdata/wsAuth.json", func(ctx context.Context, r []byte) error {
if bytes.Contains(r, []byte("%s")) {
r = fmt.Appendf(nil, string(r), optionsTradablePair.String())
}
if bytes.Contains(r, []byte("FANGLE-ACCOUNTS")) {
hold := e.Accounts
e.Accounts = nil
defer func() { e.Accounts = hold }()
}
return e.wsHandleAuthenticatedData(ctx, &FixtureConnection{match: websocket.NewMatch()}, r)
})
close(e.Websocket.DataHandler)
require.Len(t, e.Websocket.DataHandler, 6, "Should see correct number of messages")
require.Len(t, fErrs, 1, "Must get exactly one error message")
assert.ErrorContains(t, fErrs[0].Err, "cannot save holdings: nil pointer: *accounts.Accounts")
i := 0
for data := range e.Websocket.DataHandler {
@@ -3081,63 +3095,39 @@ func TestWSHandleAuthenticatedData(t *testing.T) {
assert.Equal(t, 0.358635, v[0].Fee, "fee should be correct")
assert.Equal(t, time.UnixMilli(1672364262444), v[0].Date, "Created time should be correct")
assert.Equal(t, time.UnixMilli(1672364262457), v[0].LastUpdated, "Updated time should be correct")
case []account.Change:
require.Len(t, v, 6, "must see 6 items")
for i, change := range v {
assert.Empty(t, change.Account, "Account type should be empty")
assert.Equal(t, asset.Spot, change.AssetType, "Asset type should be Spot")
require.NotNil(t, change.Balance, "balance must not be nil")
switch i {
case 0:
assert.True(t, currency.USDC.Equal(change.Balance.Currency), "currency should match")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Equal(t, 201.34882644, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 201.34882644, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 1:
assert.True(t, currency.BTC.Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 0.06488393, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 0.06488393, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 2:
assert.True(t, currency.ETH.Equal(change.Balance.Currency), "currency should match")
assert.Zero(t, change.Balance.Free, "Free should be 0")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Zero(t, change.Balance.Total, "Total should be 0")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 3:
assert.True(t, currency.USDT.Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 11728.54414904, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 11728.54414904, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 4:
assert.True(t, currency.NewCode("EOS3L").Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 215.0570412, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 215.0570412, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 5:
assert.True(t, currency.BIT.Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 1.82, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 1.82, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
}
}
case accounts.SubAccounts:
require.Len(t, v, 1, "Must have correct number of SubAccounts")
assert.Equal(t, asset.Spot, v[0].AssetType, "Asset type should be correct")
exp := accounts.CurrencyBalances{}
exp.Set(currency.ETH, accounts.Balance{
UpdatedAt: time.UnixMilli(1672364262482),
})
exp.Set(currency.USDT, accounts.Balance{
UpdatedAt: time.UnixMilli(1672364262482),
Total: 11728.54414904,
Free: 11728.54414904,
})
exp.Set(currency.EOS3L, accounts.Balance{
UpdatedAt: time.UnixMilli(1672364262482),
Total: 215.0570412,
Free: 215.0570412,
})
exp.Set(currency.BIT, accounts.Balance{
UpdatedAt: time.UnixMilli(1672364262482),
Total: 1.82,
Free: 1.82,
})
exp.Set(currency.USDC, accounts.Balance{
UpdatedAt: time.UnixMilli(1672364262482),
Total: 201.34882644,
Free: 201.34882644,
})
exp.Set(currency.BTC, accounts.Balance{
UpdatedAt: time.UnixMilli(1672364262482),
Total: 0.06488393,
Free: 0.06488393,
})
assert.Equal(t, exp, v[0].Balances, "Balances should be correct")
case *GreeksResponse:
assert.Equal(t, "592324fa945a30-2603-49a5-b865-21668c29f2a6", v.ID, "ID should be correct")
assert.Equal(t, "greeks", v.Topic, "Topic should be correct")
@@ -3162,7 +3152,7 @@ func TestWSHandleAuthenticatedData(t *testing.T) {
assert.Equal(t, 0.3374, v[0].Price, "price should be correct")
assert.Equal(t, 25.0, v[0].Amount, "amount should be correct")
default:
t.Errorf("Unexpected data received: %v", v)
t.Errorf("Unexpected data received: %T %v", v, v)
}
}
}
@@ -3170,11 +3160,11 @@ func TestWSHandleAuthenticatedData(t *testing.T) {
func TestWsTicker(t *testing.T) {
t.Parallel()
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
assetRouting := []asset.Item{
asset.Spot, asset.Options, asset.USDTMarginedFutures, asset.USDTMarginedFutures,
asset.USDCMarginedFutures, asset.USDCMarginedFutures, asset.CoinMarginedFutures, asset.CoinMarginedFutures,
}
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
testexch.FixtureToDataHandler(t, "testdata/wsTicker.json", func(_ context.Context, r []byte) error {
defer slices.Delete(assetRouting, 0, 1)
return e.wsHandleData(nil, assetRouting[0], r)
@@ -3730,7 +3720,7 @@ func TestWebsocketAuthenticatePrivateConnection(t *testing.T) {
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "dummy", Secret: "dummy"})
ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "dummy", Secret: "dummy"})
err = e.WebsocketAuthenticatePrivateConnection(ctx, &FixtureConnection{})
require.NoError(t, err)
err = e.WebsocketAuthenticatePrivateConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"success":false,"ret_msg":"failed auth","conn_id":"5758770c-8152-4545-a84f-dae089e56499","req_id":"1","op":"subscribe"}`)})
@@ -3749,7 +3739,7 @@ func TestWebsocketAuthenticateTradeConnection(t *testing.T) {
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "dummy", Secret: "dummy"})
ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "dummy", Secret: "dummy"})
err = e.WebsocketAuthenticateTradeConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"retCode":0,"retMsg":"OK","op":"auth","connId":"d2a641kgcg7ab33b7mdg-4x6a"}`)})
require.NoError(t, err)
err = e.WebsocketAuthenticateTradeConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"retCode":10004,"retMsg":"Invalid sign","op":"auth","connId":"d2a63t6p49kk82nefh90-4ye8"}`)})