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

@@ -16,33 +16,33 @@ import (
)
const (
orderbookFunc = "orderbook"
tickerFunc = "ticker"
exchangesFunc = "exchanges"
pairsFunc = "pairs"
accountInfoFunc = "accountinfo"
depositAddressFunc = "depositaddress"
orderQueryFunc = "orderquery"
orderCancelFunc = "ordercancel"
orderSubmitFunc = "ordersubmit"
withdrawCryptoFunc = "withdrawcrypto"
withdrawFiatFunc = "withdrawfiat"
ohlcvFunc = "ohlcv"
orderbookFunc = "orderbook"
tickerFunc = "ticker"
exchangesFunc = "exchanges"
pairsFunc = "pairs"
accountBalancesFunc = "accountbalances"
depositAddressFunc = "depositaddress"
orderQueryFunc = "orderquery"
orderCancelFunc = "ordercancel"
orderSubmitFunc = "ordersubmit"
withdrawCryptoFunc = "withdrawcrypto"
withdrawFiatFunc = "withdrawfiat"
ohlcvFunc = "ohlcv"
)
var exchangeModule = map[string]objects.Object{
orderbookFunc: &objects.UserFunction{Name: orderbookFunc, Value: ExchangeOrderbook},
tickerFunc: &objects.UserFunction{Name: tickerFunc, Value: ExchangeTicker},
exchangesFunc: &objects.UserFunction{Name: exchangesFunc, Value: ExchangeExchanges},
pairsFunc: &objects.UserFunction{Name: pairsFunc, Value: ExchangePairs},
accountInfoFunc: &objects.UserFunction{Name: accountInfoFunc, Value: ExchangeAccountInfo},
depositAddressFunc: &objects.UserFunction{Name: depositAddressFunc, Value: ExchangeDepositAddress},
orderQueryFunc: &objects.UserFunction{Name: orderQueryFunc, Value: ExchangeOrderQuery},
orderCancelFunc: &objects.UserFunction{Name: orderCancelFunc, Value: ExchangeOrderCancel},
orderSubmitFunc: &objects.UserFunction{Name: orderSubmitFunc, Value: ExchangeOrderSubmit},
withdrawCryptoFunc: &objects.UserFunction{Name: withdrawCryptoFunc, Value: ExchangeWithdrawCrypto},
withdrawFiatFunc: &objects.UserFunction{Name: withdrawFiatFunc, Value: ExchangeWithdrawFiat},
ohlcvFunc: &objects.UserFunction{Name: ohlcvFunc, Value: exchangeOHLCV},
orderbookFunc: &objects.UserFunction{Name: orderbookFunc, Value: ExchangeOrderbook},
tickerFunc: &objects.UserFunction{Name: tickerFunc, Value: ExchangeTicker},
exchangesFunc: &objects.UserFunction{Name: exchangesFunc, Value: ExchangeExchanges},
pairsFunc: &objects.UserFunction{Name: pairsFunc, Value: ExchangePairs},
accountBalancesFunc: &objects.UserFunction{Name: accountBalancesFunc, Value: ExchangeAccountBalances},
depositAddressFunc: &objects.UserFunction{Name: depositAddressFunc, Value: ExchangeDepositAddress},
orderQueryFunc: &objects.UserFunction{Name: orderQueryFunc, Value: ExchangeOrderQuery},
orderCancelFunc: &objects.UserFunction{Name: orderCancelFunc, Value: ExchangeOrderCancel},
orderSubmitFunc: &objects.UserFunction{Name: orderSubmitFunc, Value: ExchangeOrderSubmit},
withdrawCryptoFunc: &objects.UserFunction{Name: withdrawCryptoFunc, Value: ExchangeWithdrawCrypto},
withdrawFiatFunc: &objects.UserFunction{Name: withdrawFiatFunc, Value: ExchangeWithdrawFiat},
ohlcvFunc: &objects.UserFunction{Name: ohlcvFunc, Value: exchangeOHLCV},
}
// ExchangeOrderbook returns orderbook for requested exchange & currencypair
@@ -239,23 +239,23 @@ func ExchangePairs(args ...objects.Object) (objects.Object, error) {
return &r, nil
}
// ExchangeAccountInfo returns account information for requested exchange
func ExchangeAccountInfo(args ...objects.Object) (objects.Object, error) {
// ExchangeAccountBalances returns account balances for requested exchange
func ExchangeAccountBalances(args ...objects.Object) (objects.Object, error) {
if len(args) != 3 {
return nil, objects.ErrWrongNumArguments
}
scriptCtx, ok := objects.ToInterface(args[0]).(*Context)
if !ok {
return nil, constructRuntimeError(1, accountInfoFunc, "*gct.Context", args[0])
return nil, constructRuntimeError(1, accountBalancesFunc, "*gct.Context", args[0])
}
exchangeName, ok := objects.ToString(args[1])
if !ok {
return nil, constructRuntimeError(2, accountInfoFunc, "string", args[1])
return nil, constructRuntimeError(2, accountBalancesFunc, "string", args[1])
}
assetString, ok := objects.ToString(args[2])
if !ok {
return nil, constructRuntimeError(3, accountInfoFunc, "string", args[2])
return nil, constructRuntimeError(3, accountBalancesFunc, "string", args[2])
}
assetType, err := asset.New(assetString)
if err != nil {
@@ -263,25 +263,24 @@ func ExchangeAccountInfo(args ...objects.Object) (objects.Object, error) {
}
ctx := processScriptContext(scriptCtx)
rtnValue, err := wrappers.GetWrapper().
AccountInformation(ctx, exchangeName, assetType)
rtnValue, err := wrappers.GetWrapper().AccountBalances(ctx, exchangeName, assetType)
if err != nil {
return errorResponsef(standardFormatting, err)
}
var funds objects.Array
for x := range rtnValue.Accounts {
for y := range rtnValue.Accounts[x].Currencies {
temp := make(map[string]objects.Object, 3)
temp["name"] = &objects.String{Value: rtnValue.Accounts[x].Currencies[y].Currency.String()}
temp["total"] = &objects.Float{Value: rtnValue.Accounts[x].Currencies[y].Total}
temp["hold"] = &objects.Float{Value: rtnValue.Accounts[x].Currencies[y].Hold}
funds.Value = append(funds.Value, &objects.Map{Value: temp})
for i := range rtnValue {
for curr, bal := range rtnValue[i].Balances {
funds.Value = append(funds.Value, &objects.Map{Value: map[string]objects.Object{
"name": &objects.String{Value: curr.String()},
"total": &objects.Float{Value: bal.Total},
"hold": &objects.Float{Value: bal.Hold},
}})
}
}
data := make(map[string]objects.Object, 2)
data["exchange"] = &objects.String{Value: rtnValue.Exchange}
data["exchange"] = &objects.String{Value: exchangeName}
data["currencies"] = &funds
return &objects.Map{Value: data}, nil
}

View File

@@ -4,7 +4,7 @@ import (
"context"
objects "github.com/d5/tengo/v2"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
@@ -189,7 +189,7 @@ func processScriptContext(scriptCtx *Context) context.Context {
otp, _ = objects.ToString(object)
}
ctx = account.DeployCredentialsToContext(ctx, &account.Credentials{
ctx = accounts.DeployCredentialsToContext(ctx, &accounts.Credentials{
Key: key,
Secret: secret,
SubAccount: subAccount,
@@ -199,7 +199,7 @@ func processScriptContext(scriptCtx *Context) context.Context {
})
} else if object = scriptCtx.Value["subaccount"]; object != nil {
subAccount, _ := objects.ToString(object)
ctx = account.DeploySubAccountOverrideToContext(ctx, subAccount)
ctx = accounts.DeploySubAccountOverrideToContext(ctx, subAccount)
}
return ctx
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/gctscript/modules"
@@ -103,16 +103,16 @@ func TestExchangePairs(t *testing.T) {
assert.ErrorIs(t, err, objects.ErrWrongNumArguments)
}
func TestAccountInfo(t *testing.T) {
func TestAccountBalances(t *testing.T) {
t.Parallel()
_, err := ExchangeAccountInfo()
_, err := ExchangeAccountBalances()
assert.ErrorIs(t, err, objects.ErrWrongNumArguments)
_, err = ExchangeAccountInfo(ctx, exch, assetType)
_, err = ExchangeAccountBalances(ctx, exch, assetType)
assert.NoError(t, err)
_, err = ExchangeAccountInfo(ctx, exchError, assetType)
_, err = ExchangeAccountBalances(ctx, exchError, assetType)
assert.NoError(t, err)
}
@@ -391,7 +391,7 @@ func TestSetSubAccount(t *testing.T) {
t.Fatal("should not be nil")
}
subaccount, ok := ctx.Value(account.ContextSubAccountFlag).(string)
subaccount, ok := ctx.Value(accounts.ContextSubAccountFlag).(string)
if !ok {
t.Fatal("wrong type")
}

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -35,7 +35,7 @@ type GCTExchange interface {
QueryOrder(ctx context.Context, exch, orderid string, pair currency.Pair, assetType asset.Item) (*order.Detail, error)
SubmitOrder(ctx context.Context, submit *order.Submit) (*order.SubmitResponse, error)
CancelOrder(ctx context.Context, exch, orderid string, pair currency.Pair, item asset.Item) (bool, error)
AccountInformation(ctx context.Context, exch string, assetType asset.Item) (account.Holdings, error)
AccountBalances(ctx context.Context, exch string, assetType asset.Item) (accounts.SubAccounts, error)
DepositAddress(exch, chain string, currencyCode currency.Code) (*deposit.Address, error)
WithdrawalFiatFunds(ctx context.Context, bankAccountID string, request *withdraw.Request) (out string, err error)
WithdrawalCryptoFunds(ctx context.Context, request *withdraw.Request) (out string, err error)

View File

@@ -9,8 +9,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
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/kline"
@@ -130,16 +130,16 @@ func (e Exchange) CancelOrder(ctx context.Context, exch, orderID string, cp curr
return true, nil
}
// AccountInformation returns account information (balance etc) for requested exchange
func (e Exchange) AccountInformation(ctx context.Context, exch string, assetType asset.Item) (account.Holdings, error) {
// AccountBalances returns account balances for requested exchange
func (e Exchange) AccountBalances(ctx context.Context, exch string, assetType asset.Item) (accounts.SubAccounts, error) {
ex, err := e.GetExchange(exch)
if err != nil {
return account.Holdings{}, err
return accounts.SubAccounts{}, err
}
accountInfo, err := ex.GetCachedAccountInfo(ctx, assetType)
accountInfo, err := ex.GetCachedSubAccounts(ctx, assetType)
if err != nil {
return account.Holdings{}, err
return accounts.SubAccounts{}, err
}
return accountInfo, nil

View File

@@ -119,11 +119,11 @@ func TestExchange_Pairs(t *testing.T) {
}
}
func TestExchange_AccountInformation(t *testing.T) {
func TestExchange_AccountBalances(t *testing.T) {
if !configureExchangeKeys() {
t.Skip("no exchange configured test skipped")
}
_, err := exchangeTest.AccountInformation(t.Context(),
_, err := exchangeTest.AccountBalances(t.Context(),
exchName, asset.Spot)
if err != nil {
t.Fatal(err)

View File

@@ -159,14 +159,14 @@ func TestExchangePairs(t *testing.T) {
assert.ErrorIs(t, err, objects.ErrWrongNumArguments)
}
func TestExchangeAccountInfo(t *testing.T) {
func TestExchangeAccountBalances(t *testing.T) {
t.Parallel()
_, err := gct.ExchangeAccountInfo()
_, err := gct.ExchangeAccountBalances()
require.ErrorIs(t, err, objects.ErrWrongNumArguments)
obj, err := gct.ExchangeAccountInfo(ctx, exch, assetType)
obj, err := gct.ExchangeAccountBalances(ctx, exch, assetType)
require.NoError(t, err)
rString, ok := objects.ToString(obj)
require.True(t, ok, "ExchangeAccountInfo return value must return correctly from objects.ToString")
require.True(t, ok, "ExchangeAccountBalances return value must return correctly from objects.ToString")
require.Contains(t, rString, "Bitstamp REST or Websocket authentication support is not enabled")
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -184,32 +184,29 @@ func (w Wrapper) CancelOrder(_ context.Context, exch, orderid string, cp currenc
return true, nil
}
// AccountInformation validator for test execution/scripts
func (w Wrapper) AccountInformation(_ context.Context, exch string, assetType asset.Item) (account.Holdings, error) {
// AccountBalances validator for test execution/scripts
func (w Wrapper) AccountBalances(_ context.Context, exch string, assetType asset.Item) (accounts.SubAccounts, error) {
if exch == exchError.String() {
return account.Holdings{}, errTestFailed
return nil, errTestFailed
}
return account.Holdings{
Exchange: exch,
Accounts: []account.SubAccount{
{
ID: exch,
AssetType: assetType,
Currencies: []account.Balance{
{
Currency: currency.Code{
Item: &currency.Item{
ID: 0,
FullName: "Bitcoin",
Symbol: "BTC",
Role: 1,
AssocChain: "",
},
},
Total: 100,
Hold: 0,
},
c := currency.Code{
Item: &currency.Item{
ID: 0,
FullName: "Bitcoin",
Symbol: "BTC",
Role: 1,
AssocChain: "",
},
}
return accounts.SubAccounts{
{
ID: "subacct1",
AssetType: assetType,
Balances: accounts.CurrencyBalances{
c: accounts.Balance{
Currency: c,
Total: 100,
Hold: 0,
},
},
},

View File

@@ -4,6 +4,8 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -60,20 +62,14 @@ func TestWrapper_IsEnabled(t *testing.T) {
}
}
func TestWrapper_AccountInformation(t *testing.T) {
func TestWrapperAccountBalances(t *testing.T) {
t.Parallel()
_, err := testWrapper.AccountInformation(t.Context(),
exchName, asset.Spot)
if err != nil {
t.Fatal(err)
}
_, err := testWrapper.AccountBalances(t.Context(), exchName, asset.Spot)
require.NoError(t, err)
_, err = testWrapper.AccountInformation(t.Context(),
exchError.String(), asset.Spot)
if err == nil {
t.Fatal("expected AccountInformation to return error on invalid name")
}
_, err = testWrapper.AccountBalances(t.Context(), exchError.String(), asset.Spot)
assert.ErrorIs(t, err, errTestFailed)
}
func TestWrapper_CancelOrder(t *testing.T) {