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

@@ -121,13 +121,11 @@ func TestGetAccountByID(t *testing.T) {
assert.ErrorIs(t, err, errAccountIDEmpty)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
longResp, err := e.ListAccounts(t.Context(), 49, 0)
assert.NoError(t, err)
require.NoError(t, err)
require.True(t, longResp != nil && len(longResp.Accounts) > 0, errExpectedNonEmpty)
shortResp, err := e.GetAccountByID(t.Context(), longResp.Accounts[0].UUID)
assert.NoError(t, err)
if *shortResp != longResp.Accounts[0] {
t.Errorf(errExpectMismatch, shortResp, longResp.Accounts[0])
}
require.NoError(t, err)
assert.Equal(t, shortResp, longResp.Accounts[0])
}
func TestListAccounts(t *testing.T) {
@@ -1136,10 +1134,10 @@ func TestUpdateTradablePairs(t *testing.T) {
testexch.UpdatePairsOnce(t, e)
}
func TestUpdateAccountInfo(t *testing.T) {
func TestUpdateAccountBalances(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
resp, err := e.UpdateAccountInfo(t.Context(), asset.Spot)
resp, err := e.UpdateAccountBalances(t.Context(), asset.Spot)
require.NoError(t, err)
assert.NotEmpty(t, resp, errExpectedNonEmpty)
}
@@ -1916,10 +1914,10 @@ func convertTestHelper(t *testing.T) (fromAccID, toAccID string) {
t.Fatal(errExpectedNonEmpty)
}
for x := range accIDs.Accounts {
if accIDs.Accounts[x].Currency == testStable.String() {
if accIDs.Accounts[x].Currency == testStable {
fromAccID = accIDs.Accounts[x].UUID
}
if accIDs.Accounts[x].Currency == testFiat.String() {
if accIDs.Accounts[x].Currency == testFiat {
toAccID = accIDs.Accounts[x].UUID
}
if fromAccID != "" && toAccID != "" {

View File

@@ -46,11 +46,11 @@ type CurrencyAmount struct {
Currency currency.Code `json:"currency"`
}
// Account holds details for a trading account, returned by GetAccountByID and used as a sub-struct in the type AllAccountsResponse
// Account holds details for a trading account
type Account struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Currency string `json:"currency"`
Currency currency.Code `json:"currency"`
AvailableBalance CurrencyAmount `json:"available_balance"`
Default bool `json:"default"`
Active bool `json:"active"`
@@ -66,10 +66,10 @@ type Account struct {
// AllAccountsResponse holds many Account structs, as well as pagination information, returned by ListAccounts
type AllAccountsResponse struct {
Accounts []Account `json:"accounts"`
HasNext bool `json:"has_next"`
Cursor Integer `json:"cursor"`
Size uint8 `json:"size"`
Accounts []*Account `json:"accounts"`
HasNext bool `json:"has_next"`
Cursor Integer `json:"cursor"`
Size uint8 `json:"size"`
}
// PermissionsResponse holds information on the permissions of a user, returned by GetPermissions

View File

@@ -12,11 +12,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer"
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/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
@@ -211,48 +211,29 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context) error {
return e.EnsureOnePairEnabled()
}
// UpdateAccountInfo retrieves balances for all enabled currencies for the coinbase exchange
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
var (
response account.Holdings
accountBalance []Account
done bool
err error
cursor int64
accountResp *AllAccountsResponse
)
response.Exchange = e.Name
for !done {
if accountResp, err = e.ListAccounts(ctx, 250, cursor); err != nil {
return response, err
// UpdateAccountBalances retrieves currency balances
func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (subAccts accounts.SubAccounts, err error) {
for cursor := int64(0); ; {
resp, err := e.ListAccounts(ctx, 250, cursor)
if err != nil {
return subAccts, err
}
accountBalance = append(accountBalance, accountResp.Accounts...)
done = !accountResp.HasNext
cursor = int64(accountResp.Cursor)
for _, subAcct := range resp.Accounts {
a := accounts.NewSubAccount(assetType, subAcct.UUID)
a.Balances.Set(subAcct.Currency, accounts.Balance{
Total: subAcct.AvailableBalance.Value.Float64(),
Hold: subAcct.Hold.Value.Float64(),
Free: subAcct.AvailableBalance.Value.Float64() - subAcct.Hold.Value.Float64(),
AvailableWithoutBorrow: subAcct.AvailableBalance.Value.Float64(),
})
subAccts = subAccts.Merge(a)
}
if !resp.HasNext {
break
}
cursor = int64(resp.Cursor)
}
accountCurrencies := make(map[string][]account.Balance)
for i := range accountBalance {
profileID := accountBalance[i].UUID
currencies := accountCurrencies[profileID]
accountCurrencies[profileID] = append(currencies, account.Balance{
Currency: currency.NewCode(accountBalance[i].Currency),
Total: accountBalance[i].AvailableBalance.Value.Float64(),
Hold: accountBalance[i].Hold.Value.Float64(),
Free: accountBalance[i].AvailableBalance.Value.Float64() - accountBalance[i].Hold.Value.Float64(),
AvailableWithoutBorrow: accountBalance[i].AvailableBalance.Value.Float64(),
})
}
if response.Accounts, err = account.CollectBalances(accountCurrencies, assetType); err != nil {
return account.Holdings{}, err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&response, creds); err != nil {
return account.Holdings{}, err
}
return response, nil
return subAccts, e.Accounts.Save(ctx, subAccts, true)
}
// UpdateTickers updates all currency pairs of a given asset type
@@ -831,7 +812,7 @@ func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency
// ValidateAPICredentials validates current credentials used for wrapper functionality
func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := e.UpdateAccountInfo(ctx, assetType)
_, err := e.UpdateAccountBalances(ctx, assetType)
return e.CheckTransientError(err)
}