account: segregate holdings by credentials for future multi-key management (#956)

* exchanges/account: shift credentials to account package and segregate funds to keys

* merge: fixes

* linter: fix

* Update exchanges/account/account.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits + protection for string panic

* glorious_suggestion: add method for matching keys

* linter: fix tests

* account: add protected method for credentials minimizing access, display full account details to rpc.

* linter: spelling kweeeeeeen

* accounts/portfolio: clean/check portfolio code and quickly check balances from change. Add protected method for future matching.

* accounts: theres no point in pointerising everything

* linter: ok pointerise this then...

* exchanges: fix regression add in little notes.

* glorious: nits

* Update exchanges/account/credentials.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/account/credentials_test.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/account/credentials_test.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* gloriously: fix glorious glorious test gloriously

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
Ryan O'Hara-Reid
2022-07-21 15:05:31 +10:00
committed by GitHub
parent 455738f25f
commit 663e753f52
48 changed files with 1010 additions and 549 deletions

View File

@@ -368,6 +368,16 @@ func ({{.Variable}} *{{.CapitalName}}) UpdateAccountInfo(ctx context.Context, as
// FetchAccountInfo retrieves balances for all enabled currencies
func ({{.Variable}} *{{.CapitalName}}) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
// Example implementation below:
// creds, err := {{.Variable}}.GetCredentials(ctx)
// if err != nil {
// return account.Holdings{}, err
// }
// acc, err := account.GetHoldings({{.Variable}}.Name, creds, assetType)
// if err != nil {
// return {{.Variable}}.UpdateAccountInfo(ctx, assetType)
// }
// return acc, nil
return account.Holdings{}, common.ErrNotYetImplemented
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/core"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/gctrpc/auth"
"github.com/thrasher-corp/gocryptotrader/signaler"
"github.com/urfave/cli/v2"
@@ -28,7 +28,7 @@ var (
pairDelimiter string
certPath string
timeout time.Duration
exchangeCreds exchange.Credentials
exchangeCreds account.Credentials
verbose bool
)

View File

@@ -57,10 +57,7 @@ func setupPortfolioManager(e *ExchangeManager, portfolioManagerDelay time.Durati
// IsRunning safely checks whether the subsystem is running
func (m *portfolioManager) IsRunning() bool {
if m == nil {
return false
}
return atomic.LoadInt32(&m.started) == 1
return m != nil && atomic.LoadInt32(&m.started) == 1
}
// Start runs the subsystem
@@ -160,11 +157,10 @@ func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings)
return
}
for x := range accounts {
exchangeName := accounts[x].Exchange
var currencies []account.Balance
for y := range accounts[x].Accounts {
next:
for z := range accounts[x].Accounts[y].Currencies {
var update bool
for i := range currencies {
if !accounts[x].Accounts[y].Currencies[z].CurrencyName.Equal(currencies[i].CurrencyName) {
continue
@@ -174,10 +170,7 @@ func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings)
currencies[i].AvailableWithoutBorrow += accounts[x].Accounts[y].Currencies[z].AvailableWithoutBorrow
currencies[i].Free += accounts[x].Accounts[y].Currencies[z].Free
currencies[i].Borrowed += accounts[x].Accounts[y].Currencies[z].Borrowed
update = true
}
if update {
continue
continue next
}
currencies = append(currencies, account.Balance{
CurrencyName: accounts[x].Accounts[y].Currencies[z].CurrencyName,
@@ -190,51 +183,50 @@ func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings)
}
}
for x := range currencies {
currencyName := currencies[x].CurrencyName
total := currencies[x].Total
if !m.base.ExchangeAddressExists(exchangeName, currencyName) {
if total <= 0 {
for j := range currencies {
if !m.base.ExchangeAddressExists(accounts[x].Exchange, currencies[j].CurrencyName) {
if currencies[j].Total <= 0 {
continue
}
log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n",
exchangeName,
currencyName,
total,
accounts[x].Exchange,
currencies[j].CurrencyName,
currencies[j].Total,
portfolio.ExchangeAddress)
m.base.Addresses = append(
m.base.Addresses,
portfolio.Address{Address: exchangeName,
CoinType: currencyName,
Balance: total,
Description: portfolio.ExchangeAddress})
} else {
if total <= 0 {
log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n",
exchangeName,
currencyName)
m.base.RemoveExchangeAddress(exchangeName, currencyName)
} else {
balance, ok := m.base.GetAddressBalance(exchangeName,
portfolio.ExchangeAddress,
currencyName)
if !ok {
continue
}
m.base.Addresses = append(m.base.Addresses, portfolio.Address{
Address: accounts[x].Exchange,
CoinType: currencies[j].CurrencyName,
Balance: currencies[j].Total,
Description: portfolio.ExchangeAddress,
})
continue
}
if balance != total {
log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n",
exchangeName,
currencyName,
total)
m.base.UpdateExchangeAddressBalance(exchangeName,
currencyName,
total)
}
}
if currencies[j].Total <= 0 {
log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n",
accounts[x].Exchange,
currencies[j].CurrencyName)
m.base.RemoveExchangeAddress(accounts[x].Exchange, currencies[j].CurrencyName)
continue
}
balance, ok := m.base.GetAddressBalance(accounts[x].Exchange,
portfolio.ExchangeAddress,
currencies[j].CurrencyName)
if !ok {
continue
}
if balance != currencies[j].Total {
log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n",
accounts[x].Exchange,
currencies[j].CurrencyName,
currencies[j].Total)
m.base.UpdateExchangeAddressBalance(accounts[x].Exchange,
currencies[j].CurrencyName,
currencies[j].Total)
}
}
}

View File

@@ -112,7 +112,7 @@ func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, er
password != s.Config.RemoteControl.Password {
return ctx, fmt.Errorf("username/password mismatch")
}
ctx, err = exchange.ParseCredentialsMetadata(ctx, md)
ctx, err = account.ParseCredentialsMetadata(ctx, md)
if err != nil {
return ctx, err
}
@@ -618,7 +618,7 @@ func createAccountInfoRequest(h account.Holdings) (*gctrpc.GetAccountInfoRespons
accounts := make([]*gctrpc.Account, len(h.Accounts))
for x := range h.Accounts {
var a gctrpc.Account
a.Id = h.Accounts[x].ID
a.Id = h.Accounts[x].Credentials.String()
for _, y := range h.Accounts[x].Currencies {
if y.Total == 0 &&
y.Hold == 0 &&

View File

@@ -2198,10 +2198,12 @@ func TestGetFuturesPositions(t *testing.T) {
t.Fatalf("received '%v', expected '%v'", err, exchange.ErrCredentialsAreEmpty)
}
ctx := exchange.DeployCredentialsToContext(context.Background(), &exchange.Credentials{
Key: "wow",
Secret: "super wow",
})
ctx := account.DeployCredentialsToContext(context.Background(),
&account.Credentials{
Key: "wow",
Secret: "super wow",
},
)
_, err = s.GetFuturesPositions(ctx, &gctrpc.GetFuturesPositionsRequest{
Exchange: fakeExchangeName,
@@ -2312,7 +2314,8 @@ func TestGetCollateral(t *testing.T) {
t.Fatalf("received '%v', expected '%v'", err, exchange.ErrCredentialsAreEmpty)
}
ctx := exchange.DeployCredentialsToContext(context.Background(), &exchange.Credentials{Key: "fakerino", Secret: "supafake"})
ctx := account.DeployCredentialsToContext(context.Background(),
&account.Credentials{Key: "fakerino", Secret: "supafake"})
_, err = s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{
Exchange: fakeExchangeName,
@@ -2322,7 +2325,8 @@ func TestGetCollateral(t *testing.T) {
t.Fatalf("received '%v', expected '%v'", err, errNoAccountInformation)
}
ctx = exchange.DeployCredentialsToContext(context.Background(), &exchange.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "1337"})
ctx = account.DeployCredentialsToContext(context.Background(),
&account.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "1337"})
r, err := s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{
Exchange: fakeExchangeName,

View File

@@ -26,6 +26,8 @@ var (
errNoExchangeSubAccountBalances = errors.New("no exchange sub account balances")
errNoBalanceFound = errors.New("no balance found")
errBalanceIsNil = errors.New("balance is nil")
errNoCredentialBalances = errors.New("no balances associated with credentials")
errCredentialsAreNil = errors.New("credentials are nil")
)
// CollectBalances converts a map of sub-account balances into a slice
@@ -64,16 +66,22 @@ func SubscribeToExchangeAccount(exchange string) (dispatch.Pipe, error) {
}
// Process processes new account holdings updates
func Process(h *Holdings) error {
return service.Update(h)
func Process(h *Holdings, c *Credentials) error {
return service.Update(h, c)
}
// GetHoldings returns full holdings for an exchange
func GetHoldings(exch string, assetType asset.Item) (Holdings, error) {
// GetHoldings returns full holdings for an exchange.
// NOTE: Due to credentials these amounts could be N*APIKEY actual holdings.
// TODO: Add jurisdiction and differentiation between APIKEY holdings.
func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holdings, error) {
if exch == "" {
return Holdings{}, errExchangeNameUnset
}
if creds.IsEmpty() {
return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, errCredentialsAreNil)
}
if !assetType.IsValid() {
return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, asset.ErrNotSupported)
}
@@ -88,7 +96,16 @@ func GetHoldings(exch string, assetType asset.Item) (Holdings, error) {
}
var accountsHoldings []SubAccount
for subAccount, assetHoldings := range accounts.SubAccounts {
subAccountHoldings, ok := accounts.SubAccounts[*creds]
if !ok {
return Holdings{}, fmt.Errorf("%s %s %s %w",
exch,
creds,
assetType,
errNoCredentialBalances)
}
for subAccount, assetHoldings := range subAccountHoldings {
for ai, currencyHoldings := range assetHoldings {
if ai != assetType {
continue
@@ -113,10 +130,16 @@ func GetHoldings(exch string, assetType asset.Item) (Holdings, error) {
continue
}
cpy := *creds
if cpy.SubAccount == "" {
cpy.SubAccount = subAccount
}
accountsHoldings = append(accountsHoldings, SubAccount{
ID: subAccount,
AssetType: ai,
Currencies: currencyBalances,
Credentials: Protected{creds: cpy},
ID: subAccount,
AssetType: ai,
Currencies: currencyBalances,
})
break
}
@@ -132,22 +155,24 @@ func GetHoldings(exch string, assetType asset.Item) (Holdings, error) {
}
// GetBalance returns the internal balance for that asset item.
func GetBalance(exch, subAccount string, ai asset.Item, c currency.Code) (*ProtectedBalance, error) {
func GetBalance(exch, subAccount string, creds *Credentials, ai asset.Item, c currency.Code) (*ProtectedBalance, error) {
if exch == "" {
return nil, errExchangeNameUnset
return nil, fmt.Errorf("cannot get balance: %w", errExchangeNameUnset)
}
if !ai.IsValid() {
return nil, fmt.Errorf("%s %w", ai, asset.ErrNotSupported)
return nil, fmt.Errorf("cannot get balance: %s %w", ai, asset.ErrNotSupported)
}
if creds.IsEmpty() {
return nil, fmt.Errorf("cannot get balance: %w", errCredentialsAreNil)
}
if c.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
return nil, fmt.Errorf("cannot get balance: %w", currency.ErrCurrencyCodeEmpty)
}
exch = strings.ToLower(exch)
subAccount = strings.ToLower(subAccount)
service.mu.Lock()
defer service.mu.Unlock()
@@ -156,7 +181,13 @@ func GetBalance(exch, subAccount string, ai asset.Item, c currency.Code) (*Prote
return nil, fmt.Errorf("%s %w", exch, errExchangeHoldingsNotFound)
}
assetBalances, ok := accounts.SubAccounts[subAccount]
subAccounts, ok := accounts.SubAccounts[*creds]
if !ok {
return nil, fmt.Errorf("%s %s %w",
exch, creds, errNoCredentialBalances)
}
assetBalances, ok := subAccounts[subAccount]
if !ok {
return nil, fmt.Errorf("%s %s %w",
exch, subAccount, errNoExchangeSubAccountBalances)
@@ -177,16 +208,20 @@ func GetBalance(exch, subAccount string, ai asset.Item, c currency.Code) (*Prote
}
// Update updates holdings with new account info
func (s *Service) Update(a *Holdings) error {
if a == nil {
return errHoldingsIsNil
func (s *Service) Update(incoming *Holdings, creds *Credentials) error {
if incoming == nil {
return fmt.Errorf("cannot update holdings: %w", errHoldingsIsNil)
}
if a.Exchange == "" {
return errExchangeNameUnset
if incoming.Exchange == "" {
return fmt.Errorf("cannot update holdings: %w", errExchangeNameUnset)
}
exch := strings.ToLower(a.Exchange)
if creds.IsEmpty() {
return fmt.Errorf("cannot update holdings: %w", errCredentialsAreNil)
}
exch := strings.ToLower(incoming.Exchange)
s.mu.Lock()
defer s.mu.Unlock()
accounts, ok := s.exchangeAccounts[exch]
@@ -197,46 +232,65 @@ func (s *Service) Update(a *Holdings) error {
}
accounts = &Accounts{
ID: id,
SubAccounts: make(map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance),
SubAccounts: make(map[Credentials]map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance),
}
s.exchangeAccounts[exch] = accounts
}
var errs common.Errors
for x := range a.Accounts {
if !a.Accounts[x].AssetType.IsValid() {
for x := range incoming.Accounts {
if !incoming.Accounts[x].AssetType.IsValid() {
errs = append(errs, fmt.Errorf("cannot load sub account holdings for %s [%s] %w",
a.Accounts[x].ID,
a.Accounts[x].AssetType,
incoming.Accounts[x].ID,
incoming.Accounts[x].AssetType,
asset.ErrNotSupported))
continue
}
lowerSA := strings.ToLower(a.Accounts[x].ID)
// This assignment outside of scope is designed to have minimal impact
// on the exchange implementation UpdateAccountInfo() and portfoio
// management.
// TODO: Update incoming Holdings type to already be populated. (Suggestion)
cpy := *creds
if cpy.SubAccount == "" {
cpy.SubAccount = incoming.Accounts[x].ID
}
incoming.Accounts[x].Credentials.creds = cpy
var subAccounts map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance
subAccounts, ok = accounts.SubAccounts[*creds]
if !ok {
subAccounts = make(map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance)
accounts.SubAccounts[*creds] = subAccounts
}
var accountAssets map[asset.Item]map[*currency.Item]*ProtectedBalance
accountAssets, ok = accounts.SubAccounts[lowerSA]
accountAssets, ok = subAccounts[incoming.Accounts[x].ID]
if !ok {
accountAssets = make(map[asset.Item]map[*currency.Item]*ProtectedBalance)
accounts.SubAccounts[lowerSA] = accountAssets
// Note: Sub accounts are case sensitive and an account "name" is
// different to account "naMe".
subAccounts[incoming.Accounts[x].ID] = accountAssets
}
var currencyBalances map[*currency.Item]*ProtectedBalance
currencyBalances, ok = accountAssets[a.Accounts[x].AssetType]
currencyBalances, ok = accountAssets[incoming.Accounts[x].AssetType]
if !ok {
currencyBalances = make(map[*currency.Item]*ProtectedBalance)
accountAssets[a.Accounts[x].AssetType] = currencyBalances
accountAssets[incoming.Accounts[x].AssetType] = currencyBalances
}
for y := range a.Accounts[x].Currencies {
bal := currencyBalances[a.Accounts[x].Currencies[y].CurrencyName.Item]
for y := range incoming.Accounts[x].Currencies {
bal := currencyBalances[incoming.Accounts[x].Currencies[y].CurrencyName.Item]
if bal == nil {
bal = &ProtectedBalance{}
currencyBalances[a.Accounts[x].Currencies[y].CurrencyName.Item] = bal
currencyBalances[incoming.Accounts[x].Currencies[y].CurrencyName.Item] = bal
}
bal.load(a.Accounts[x].Currencies[y])
bal.load(incoming.Accounts[x].Currencies[y])
}
}
err := s.mux.Publish(a, accounts.ID)
err := s.mux.Publish(incoming, accounts.ID)
if err != nil {
return err
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
var happyCredentials = &Credentials{Key: "AAAAA"}
func TestCollectBalances(t *testing.T) {
t.Parallel()
accounts, err := CollectBalances(
@@ -63,12 +65,12 @@ func TestGetHoldings(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = Process(nil)
err = Process(nil, nil)
if !errors.Is(err, errHoldingsIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errHoldingsIsNil)
}
err = Process(&Holdings{})
err = Process(&Holdings{}, nil)
if !errors.Is(err, errExchangeNameUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset)
}
@@ -77,9 +79,14 @@ func TestGetHoldings(t *testing.T) {
Exchange: "Test",
}
err = Process(&holdings)
if err != nil {
t.Error(err)
err = Process(&holdings, nil)
if !errors.Is(err, errCredentialsAreNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errCredentialsAreNil)
}
err = Process(&holdings, happyCredentials)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = Process(&Holdings{
@@ -88,7 +95,7 @@ func TestGetHoldings(t *testing.T) {
{
ID: "1337",
}},
})
}, happyCredentials)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
@@ -111,7 +118,7 @@ func TestGetHoldings(t *testing.T) {
},
},
}},
})
}, happyCredentials)
if err != nil {
t.Error(err)
}
@@ -131,32 +138,42 @@ func TestGetHoldings(t *testing.T) {
},
},
}},
})
}, happyCredentials)
if err != nil {
t.Error(err)
}
_, err = GetHoldings("", asset.Spot)
_, err = GetHoldings("", nil, asset.Spot)
if !errors.Is(err, errExchangeNameUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset)
}
_, err = GetHoldings("bla", asset.Spot)
_, err = GetHoldings("bla", nil, asset.Spot)
if !errors.Is(err, errCredentialsAreNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errCredentialsAreNil)
}
_, err = GetHoldings("bla", happyCredentials, asset.Spot)
if !errors.Is(err, errExchangeHoldingsNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeHoldingsNotFound)
}
_, err = GetHoldings("bla", asset.Empty)
_, err = GetHoldings("bla", happyCredentials, asset.Empty)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
_, err = GetHoldings("Test", asset.UpsideProfitContract)
_, err = GetHoldings("Test", happyCredentials, asset.UpsideProfitContract)
if !errors.Is(err, errAssetHoldingsNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, errAssetHoldingsNotFound)
}
u, err := GetHoldings("Test", asset.Spot)
_, err = GetHoldings("Test", &Credentials{Key: "BBBBB"}, asset.Spot)
if !errors.Is(err, errNoCredentialBalances) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoCredentialBalances)
}
u, err := GetHoldings("Test", happyCredentials, asset.Spot)
if err != nil {
t.Error(err)
}
@@ -217,7 +234,7 @@ func TestGetHoldings(t *testing.T) {
},
},
}},
})
}, happyCredentials)
if err != nil {
t.Error(err)
}
@@ -226,22 +243,27 @@ func TestGetHoldings(t *testing.T) {
}
func TestGetBalance(t *testing.T) {
_, err := GetBalance("", "", asset.Empty, currency.Code{})
_, err := GetBalance("", "", nil, asset.Empty, currency.Code{})
if !errors.Is(err, errExchangeNameUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset)
}
_, err = GetBalance("bruh", "", asset.Empty, currency.Code{})
_, err = GetBalance("bruh", "", nil, asset.Empty, currency.Code{})
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
_, err = GetBalance("bruh", "", asset.Spot, currency.Code{})
_, err = GetBalance("bruh", "", nil, asset.Spot, currency.Code{})
if !errors.Is(err, errCredentialsAreNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errCredentialsAreNil)
}
_, err = GetBalance("bruh", "", happyCredentials, asset.Spot, currency.Code{})
if !errors.Is(err, currency.ErrCurrencyCodeEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrCurrencyCodeEmpty)
}
_, err = GetBalance("bruh", "", asset.Spot, currency.BTC)
_, err = GetBalance("bruh", "", happyCredentials, asset.Spot, currency.BTC)
if !errors.Is(err, errExchangeHoldingsNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeHoldingsNotFound)
}
@@ -254,22 +276,27 @@ func TestGetBalance(t *testing.T) {
ID: "1337",
},
},
})
}, happyCredentials)
if err != nil {
t.Error(err)
}
_, err = GetBalance("bruh", "1336", asset.Spot, currency.BTC)
_, err = GetBalance("bruh", "1336", &Credentials{Key: "BBBBB"}, asset.Spot, currency.BTC)
if !errors.Is(err, errNoCredentialBalances) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoCredentialBalances)
}
_, err = GetBalance("bruh", "1336", happyCredentials, asset.Spot, currency.BTC)
if !errors.Is(err, errNoExchangeSubAccountBalances) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoExchangeSubAccountBalances)
}
_, err = GetBalance("bruh", "1337", asset.Futures, currency.BTC)
_, err = GetBalance("bruh", "1337", happyCredentials, asset.Futures, currency.BTC)
if !errors.Is(err, errAssetHoldingsNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, errAssetHoldingsNotFound)
}
_, err = GetBalance("bruh", "1337", asset.Spot, currency.BTC)
_, err = GetBalance("bruh", "1337", happyCredentials, asset.Spot, currency.BTC)
if !errors.Is(err, errNoBalanceFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoBalanceFound)
}
@@ -289,12 +316,12 @@ func TestGetBalance(t *testing.T) {
},
},
},
})
}, happyCredentials)
if err != nil {
t.Error(err)
}
bal, err := GetBalance("bruh", "1337", asset.Spot, currency.BTC)
bal, err := GetBalance("bruh", "1337", happyCredentials, asset.Spot, currency.BTC)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
@@ -379,12 +406,12 @@ func TestGetFree(t *testing.T) {
func TestUpdate(t *testing.T) {
t.Parallel()
s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux(nil)}
err := s.Update(nil)
err := s.Update(nil, nil)
if !errors.Is(err, errHoldingsIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errHoldingsIsNil)
}
err = s.Update(&Holdings{})
err = s.Update(&Holdings{}, nil)
if !errors.Is(err, errExchangeNameUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset)
}
@@ -416,7 +443,7 @@ func TestUpdate(t *testing.T) {
},
},
},
})
}, happyCredentials)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
@@ -436,7 +463,7 @@ func TestUpdate(t *testing.T) {
},
},
},
})
}, happyCredentials)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
@@ -446,7 +473,7 @@ func TestUpdate(t *testing.T) {
t.Fatal("account should be loaded")
}
b, ok := acc.SubAccounts["1337"][asset.Spot][currency.BTC.Item]
b, ok := acc.SubAccounts[Credentials{Key: "AAAAA"}]["1337"][asset.Spot][currency.BTC.Item]
if !ok {
t.Fatal("account should be loaded")
}

View File

@@ -26,8 +26,13 @@ type Service struct {
// Accounts holds a stream ID and a map to the exchange holdings
type Accounts struct {
ID uuid.UUID
SubAccounts map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance
ID uuid.UUID
// NOTE: Credentials is a place holder for a future interface type, which
// will need -
// TODO: Credential tracker to match to keys that are managed and return
// pointer.
// TODO: Have different cred struct for centralized verse DEFI exchanges.
SubAccounts map[Credentials]map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance
}
// Holdings is a generic type to hold each exchange's holdings for all enabled
@@ -39,9 +44,10 @@ type Holdings struct {
// SubAccount defines a singular account type with associated currency balances
type SubAccount struct {
ID string
AssetType asset.Item
Currencies []Balance
Credentials Protected
ID string
AssetType asset.Item
Currencies []Balance
}
// Balance is a sub type to store currency name and individual totals
@@ -76,3 +82,9 @@ type ProtectedBalance struct {
// usage.
notice alert.Notice
}
// Protected limits the access to the underlying credentials outside of this
// package.
type Protected struct {
creds Credentials
}

View File

@@ -0,0 +1,218 @@
package account
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"google.golang.org/grpc/metadata"
)
// contextCredential is a string flag for use with context values when setting
// credentials internally or via gRPC.
type contextCredential string
const (
// ContextCredentialsFlag used for retrieving api credentials from context
ContextCredentialsFlag contextCredential = "apicredentials"
// ContextSubAccountFlag used for retrieving just the sub account from
// context, when the default config credentials sub account needs to be
// changed while the same keys can be used.
ContextSubAccountFlag contextCredential = "subaccountoverride"
Key = "key"
Secret = "secret"
SubAccountSTR = "subaccount"
ClientID = "clientid"
OneTimePassword = "otp"
PEMKey = "pemkey"
apiKeyDisplaySize = 16
)
var (
errMetaDataIsNil = errors.New("meta data is nil")
errInvalidCredentialMetaDataLength = errors.New("invalid meta data to process credentials")
errMissingInfo = errors.New("cannot parse meta data missing information in key value pair")
)
// Credentials define parameters that allow for an authenticated request.
type Credentials struct {
Key string
Secret string
ClientID string // TODO: Implement with exchange orders functionality
PEMKey string
SubAccount string
OneTimePassword string
// TODO: Add AccessControl uint8 for READ/WRITE/Withdraw capabilities.
}
// GetMetaData returns the credentials for metadata context deployment
func (c *Credentials) GetMetaData() (flag, values string) {
vals := make([]string, 0, 6)
if c.Key != "" {
vals = append(vals, Key+":"+c.Key)
}
if c.Secret != "" {
vals = append(vals, Secret+":"+c.Secret)
}
if c.SubAccount != "" {
vals = append(vals, SubAccountSTR+":"+c.SubAccount)
}
if c.ClientID != "" {
vals = append(vals, ClientID+":"+c.ClientID)
}
if c.PEMKey != "" {
vals = append(vals, PEMKey+":"+c.PEMKey)
}
if c.OneTimePassword != "" {
vals = append(vals, OneTimePassword+":"+c.OneTimePassword)
}
return string(ContextCredentialsFlag), strings.Join(vals, ",")
}
// String prints out basic credential info (obfuscated) to track key instances
// associated with exchanges.
func (c *Credentials) String() string {
obfuscated := c.Key
if len(obfuscated) > apiKeyDisplaySize {
obfuscated = obfuscated[:apiKeyDisplaySize]
}
return fmt.Sprintf("Key:[%s...] SubAccount:[%s] ClientID:[%s]",
obfuscated,
c.SubAccount,
c.ClientID)
}
// getInternal returns the values for assignment to an internal context
func (c *Credentials) getInternal() (contextCredential, *ContextCredentialsStore) {
if c.IsEmpty() {
return "", nil
}
store := &ContextCredentialsStore{}
store.Load(c)
return ContextCredentialsFlag, store
}
// IsEmpty return true if the underlying credentials type has not been filled
// with at least one item.
func (c *Credentials) IsEmpty() bool {
return c == nil || c.ClientID == "" &&
c.Key == "" &&
c.OneTimePassword == "" &&
c.PEMKey == "" &&
c.Secret == "" &&
c.SubAccount == ""
}
// Equal determines if the keys are the same.
// OTP omitted because it's generated per request.
// PEMKey and Secret omitted because of direct correlation with api key.
func (c *Credentials) Equal(other *Credentials) bool {
return c != nil &&
other != nil &&
c.Key == other.Key &&
c.ClientID == other.ClientID &&
c.SubAccount == other.SubAccount
}
// ContextCredentialsStore protects the stored credentials for use in a context
type ContextCredentialsStore struct {
creds *Credentials
mu sync.RWMutex
}
// Load stores provided credentials
func (c *ContextCredentialsStore) Load(creds *Credentials) {
// Segregate from external call
cpy := *creds
c.mu.Lock()
c.creds = &cpy
c.mu.Unlock()
}
// Get returns the full credentials from the store
func (c *ContextCredentialsStore) Get() *Credentials {
c.mu.RLock()
creds := *c.creds
c.mu.RUnlock()
return &creds
}
// ParseCredentialsMetadata intercepts and converts credentials metadata to a
// static type for authentication processing and protection.
func ParseCredentialsMetadata(ctx context.Context, md metadata.MD) (context.Context, error) {
if md == nil {
return ctx, errMetaDataIsNil
}
credMD, ok := md[string(ContextCredentialsFlag)]
if !ok || len(credMD) == 0 {
return ctx, nil
}
if len(credMD) != 1 {
return ctx, errInvalidCredentialMetaDataLength
}
segregatedCreds := strings.Split(credMD[0], ",")
var ctxCreds Credentials
var subAccountHere string
for x := range segregatedCreds {
keyVals := strings.Split(segregatedCreds[x], ":")
if len(keyVals) != 2 {
return ctx, fmt.Errorf("%w received %v fields, expected 2 contains: %s",
errMissingInfo,
len(keyVals),
keyVals)
}
switch keyVals[0] {
case Key:
ctxCreds.Key = keyVals[1]
case Secret:
ctxCreds.Secret = keyVals[1]
case SubAccountSTR:
// Capture sub account as this can override if other values are
// not included in metadata.
subAccountHere = keyVals[1]
case ClientID:
ctxCreds.ClientID = keyVals[1]
case PEMKey:
ctxCreds.PEMKey = keyVals[1]
case OneTimePassword:
ctxCreds.OneTimePassword = keyVals[1]
}
}
if ctxCreds.IsEmpty() && subAccountHere != "" {
// This will override default sub account details if needed.
return deploySubAccountOverrideToContext(ctx, subAccountHere), nil
}
// merge sub account to main context credentials
ctxCreds.SubAccount = subAccountHere
return DeployCredentialsToContext(ctx, &ctxCreds), nil
}
// DeployCredentialsToContext sets credentials for internal use to context which
// can override default credential values.
func DeployCredentialsToContext(ctx context.Context, creds *Credentials) context.Context {
flag, store := creds.getInternal()
return context.WithValue(ctx, flag, store)
}
// deploySubAccountOverrideToContext sets subaccount as override to credentials
// as a separate flag.
func deploySubAccountOverrideToContext(ctx context.Context, subAccount string) context.Context {
return context.WithValue(ctx, ContextSubAccountFlag, subAccount)
}
// String strings the credentials in a protected way.
func (p *Protected) String() string {
return p.creds.String()
}
// Equal determines if the keys are the same
func (p *Protected) Equal(other *Credentials) bool {
return p.creds.Equal(other)
}

View File

@@ -0,0 +1,240 @@
package account
import (
"context"
"errors"
"testing"
"google.golang.org/grpc/metadata"
)
func TestIsEmpty(t *testing.T) {
t.Parallel()
var c *Credentials
if !c.IsEmpty() {
t.Fatalf("expected: %v but received: %v", true, c.IsEmpty())
}
c = new(Credentials)
if !c.IsEmpty() {
t.Fatalf("expected: %v but received: %v", true, c.IsEmpty())
}
c.SubAccount = "woow"
if c.IsEmpty() {
t.Fatalf("expected: %v but received: %v", false, c.IsEmpty())
}
}
func TestParseCredentialsMetadata(t *testing.T) {
t.Parallel()
_, err := ParseCredentialsMetadata(context.Background(), nil)
if !errors.Is(err, errMetaDataIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errMetaDataIsNil)
}
_, err = ParseCredentialsMetadata(context.Background(), metadata.MD{})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
ctx := metadata.AppendToOutgoingContext(context.Background(),
string(ContextCredentialsFlag), "wow", string(ContextCredentialsFlag), "wow2")
nortyMD, _ := metadata.FromOutgoingContext(ctx)
_, err = ParseCredentialsMetadata(context.Background(), nortyMD)
if !errors.Is(err, errInvalidCredentialMetaDataLength) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCredentialMetaDataLength)
}
ctx = metadata.AppendToOutgoingContext(context.Background(),
string(ContextCredentialsFlag), "brokenstring")
nortyMD, _ = metadata.FromOutgoingContext(ctx)
_, err = ParseCredentialsMetadata(context.Background(), nortyMD)
if !errors.Is(err, errMissingInfo) {
t.Fatalf("received: '%v' but expected: '%v'", err, errMissingInfo)
}
beforeCreds := Credentials{
Key: "superkey",
Secret: "supersecret",
SubAccount: "supersub",
ClientID: "superclient",
PEMKey: "superpem",
OneTimePassword: "superOneTimePasssssss",
}
flag, outGoing := beforeCreds.GetMetaData()
ctx = metadata.AppendToOutgoingContext(context.Background(), flag, outGoing)
lovelyMD, _ := metadata.FromOutgoingContext(ctx)
ctx, err = ParseCredentialsMetadata(context.Background(), lovelyMD)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
store, ok := ctx.Value(ContextCredentialsFlag).(*ContextCredentialsStore)
if !ok {
t.Fatal("should have processed")
}
afterCreds := store.Get()
if afterCreds.Key != "superkey" &&
afterCreds.Secret != "supersecret" &&
afterCreds.SubAccount != "supersub" &&
afterCreds.ClientID != "superclient" &&
afterCreds.PEMKey != "superpem" &&
afterCreds.OneTimePassword != "superOneTimePasssssss" {
t.Fatal("unexpected values")
}
// subaccount override
subaccount := Credentials{
SubAccount: "supersub",
}
flag, outGoing = subaccount.GetMetaData()
ctx = metadata.AppendToOutgoingContext(context.Background(), flag, outGoing)
lovelyMD, _ = metadata.FromOutgoingContext(ctx)
ctx, err = ParseCredentialsMetadata(context.Background(), lovelyMD)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
sa, ok := ctx.Value(ContextSubAccountFlag).(string)
if !ok {
t.Fatal("should have processed")
}
if sa != "supersub" {
t.Fatal("unexpected value")
}
}
func TestGetInternal(t *testing.T) {
t.Parallel()
flag, store := (&Credentials{}).getInternal()
if flag != "" {
t.Fatal("unexpected value")
}
if store != nil {
t.Fatal("unexpected value")
}
flag, store = (&Credentials{Key: "wow"}).getInternal()
if flag != ContextCredentialsFlag {
t.Fatal("unexpected value")
}
if store == nil {
t.Fatal("unexpected value")
}
if store.Get().Key != "wow" {
t.Fatal("unexpected value")
}
}
func TestString(t *testing.T) {
t.Parallel()
creds := Credentials{}
if s := creds.String(); s != "Key:[...] SubAccount:[] ClientID:[]" {
t.Fatal("unexpected value")
}
creds.Key = "12345678910111234"
creds.SubAccount = "sub"
creds.ClientID = "client"
if s := creds.String(); s != "Key:[1234567891011123...] SubAccount:[sub] ClientID:[client]" {
t.Fatal("unexpected value")
}
}
func TestCredentialsEqual(t *testing.T) {
t.Parallel()
var this, that *Credentials
if this.Equal(that) {
t.Fatal("unexpected value")
}
this = &Credentials{}
if this.Equal(that) {
t.Fatal("unexpected value")
}
that = &Credentials{Key: "1337"}
if this.Equal(that) {
t.Fatal("unexpected value")
}
this.Key = "1337"
if !this.Equal(that) {
t.Fatal("unexpected value")
}
this.ClientID = "1337"
if this.Equal(that) {
t.Fatal("unexpected value")
}
that.ClientID = "1337"
if !this.Equal(that) {
t.Fatal("unexpected value")
}
this.SubAccount = "someSub"
if this.Equal(that) {
t.Fatal("unexpected value")
}
that.SubAccount = "someSub"
if !this.Equal(that) {
t.Fatal("unexpected value")
}
}
func TestProtectedString(t *testing.T) {
t.Parallel()
p := Protected{}
if s := p.String(); s != "Key:[...] SubAccount:[] ClientID:[]" {
t.Fatal("unexpected value")
}
p.creds.Key = "12345678910111234"
p.creds.SubAccount = "sub"
p.creds.ClientID = "client"
if s := p.creds.String(); s != "Key:[1234567891011123...] SubAccount:[sub] ClientID:[client]" {
t.Fatal("unexpected value")
}
}
func TestProtectedCredentialsEqual(t *testing.T) {
t.Parallel()
var this Protected
var that *Credentials
if this.Equal(that) {
t.Fatal("unexpected value")
}
this.creds = Credentials{}
if this.Equal(that) {
t.Fatal("unexpected value")
}
that = &Credentials{Key: "1337"}
if this.Equal(that) {
t.Fatal("unexpected value")
}
this.creds.Key = "1337"
if !this.Equal(that) {
t.Fatal("unexpected value")
}
this.creds.ClientID = "1337"
if this.Equal(that) {
t.Fatal("unexpected value")
}
that.ClientID = "1337"
if !this.Equal(that) {
t.Fatal("unexpected value")
}
this.creds.SubAccount = "someSub"
if this.Equal(that) {
t.Fatal("unexpected value")
}
that.SubAccount = "someSub"
if !this.Equal(that) {
t.Fatal("unexpected value")
}
}

View File

@@ -116,7 +116,12 @@ func (a *Alphapoint) UpdateAccountInfo(ctx context.Context, assetType asset.Item
AssetType: assetType,
})
err = account.Process(&response)
creds, err := a.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -127,7 +132,11 @@ func (a *Alphapoint) UpdateAccountInfo(ctx context.Context, assetType asset.Item
// FetchAccountInfo retrieves balances for all enabled currencies on the
// Alphapoint exchange
func (a *Alphapoint) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(a.Name, assetType)
creds, err := a.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(a.Name, creds, assetType)
if err != nil {
return a.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -788,7 +788,11 @@ func (b *Binance) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
}
acc.AssetType = assetType
info.Accounts = append(info.Accounts, acc)
if err := account.Process(&info); err != nil {
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
return info, nil
@@ -796,7 +800,11 @@ func (b *Binance) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Binance) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -519,7 +519,11 @@ func (b *Bitfinex) UpdateAccountInfo(ctx context.Context, assetType asset.Item)
}
response.Accounts = Accounts
err = account.Process(&response)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -529,11 +533,14 @@ func (b *Bitfinex) UpdateAccountInfo(ctx context.Context, assetType asset.Item)
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Bitfinex) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -320,11 +320,14 @@ func (b *Bitflyer) UpdateAccountInfo(_ context.Context, _ asset.Item) (account.H
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Bitflyer) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -394,7 +394,11 @@ func (b *Bithumb) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
})
info.Exchange = b.Name
err = account.Process(&info)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&info, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -404,11 +408,14 @@ func (b *Bithumb) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Bithumb) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -457,7 +457,11 @@ func (b *Bitmex) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
}
info.Exchange = b.Name
if err := account.Process(&info); err != nil {
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
@@ -466,11 +470,14 @@ func (b *Bitmex) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Bitmex) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -452,7 +452,11 @@ func (b *Bitstamp) UpdateAccountInfo(ctx context.Context, assetType asset.Item)
Currencies: currencies,
})
err = account.Process(&response)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -462,11 +466,14 @@ func (b *Bitstamp) UpdateAccountInfo(ctx context.Context, assetType asset.Item)
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Bitstamp) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -425,12 +425,20 @@ func (b *Bittrex) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
})
resp.Exchange = b.Name
return resp, account.Process(&resp)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
return resp, account.Process(&resp, creds)
}
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *Bittrex) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
resp, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
resp, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -457,7 +457,11 @@ func (b *BTCMarkets) UpdateAccountInfo(ctx context.Context, assetType asset.Item
resp.Accounts = append(resp.Accounts, acc)
resp.Exchange = b.Name
err = account.Process(&resp)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&resp, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -467,7 +471,11 @@ func (b *BTCMarkets) UpdateAccountInfo(ctx context.Context, assetType asset.Item
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *BTCMarkets) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -416,7 +416,11 @@ func (b *BTSE) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc
},
}
err = account.Process(&a)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&a, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -426,7 +430,11 @@ func (b *BTSE) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc
// FetchAccountInfo retrieves balances for all enabled currencies
func (b *BTSE) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(b.Name, assetType)
creds, err := b.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(b.Name, creds, assetType)
if err != nil {
return b.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
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/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -401,7 +401,7 @@ func (c *CoinbasePro) GenerateDefaultSubscriptions() ([]stream.ChannelSubscripti
// Subscribe sends a websocket message to receive data from the channel
func (c *CoinbasePro) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var creds *exchange.Credentials
var creds *account.Credentials
var err error
if c.IsWebsocketAuthenticationSupported() {
creds, err = c.GetCredentials(context.TODO())

View File

@@ -346,7 +346,11 @@ func (c *CoinbasePro) UpdateAccountInfo(ctx context.Context, assetType asset.Ite
return account.Holdings{}, err
}
err = account.Process(&response)
creds, err := c.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -356,11 +360,14 @@ func (c *CoinbasePro) UpdateAccountInfo(ctx context.Context, assetType asset.Ite
// FetchAccountInfo retrieves balances for all enabled currencies
func (c *CoinbasePro) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(c.Name, assetType)
creds, err := c.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(c.Name, creds, assetType)
if err != nil {
return c.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
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/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
@@ -275,7 +276,7 @@ func (c *COINUT) SendHTTPRequest(ctx context.Context, ep exchange.URL, apiReques
headers := make(map[string]string)
if authenticated {
var creds *exchange.Credentials
var creds *account.Credentials
creds, err = c.GetCredentials(ctx)
if err != nil {
return nil, err

View File

@@ -16,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency"
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/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
@@ -832,7 +833,8 @@ func TestWsLogin(t *testing.T) {
"unverified_email":"",
"username":"test"
}`)
ctx := exchange.DeployCredentialsToContext(context.Background(), &exchange.Credentials{Key: "b46e658f-d4c4-433c-b032-093423b1aaa4", ClientID: "dummy"})
ctx := account.DeployCredentialsToContext(context.Background(),
&account.Credentials{Key: "b46e658f-d4c4-433c-b032-093423b1aaa4", ClientID: "dummy"})
err := c.wsHandleData(ctx, pressXToJSON)
if err != nil {
t.Error(err)

View File

@@ -395,7 +395,11 @@ func (c *COINUT) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
Currencies: balances,
})
err = account.Process(&info)
creds, err := c.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&info, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -405,11 +409,14 @@ func (c *COINUT) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
// FetchAccountInfo retrieves balances for all enabled currencies
func (c *COINUT) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(c.Name, assetType)
creds, err := c.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(c.Name, creds, assetType)
if err != nil {
return c.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -5,28 +5,11 @@ import (
"errors"
"fmt"
"strings"
"sync"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/log"
"google.golang.org/grpc/metadata"
)
// contextCredential is a string flag for use with context values when setting
// credentials internally or via gRPC.
type contextCredential string
const (
contextCredentialsFlag contextCredential = "apicredentials"
contextSubAccountFlag contextCredential = "subaccountoverride"
key = "key"
secret = "secret"
subAccount = "subaccount"
clientID = "clientid"
oneTimePassword = "otp"
_PEMKey = "pemkey"
)
var (
@@ -39,167 +22,20 @@ var (
// undertake an authenticated HTTP request.
ErrCredentialsAreEmpty = errors.New("credentials are empty")
errRequiresAPIKey = errors.New("requires API key but default/empty one set")
errRequiresAPISecret = errors.New("requires API secret but default/empty one set")
errRequiresAPIPEMKey = errors.New("requires API PEM key but default/empty one set")
errRequiresAPIClientID = errors.New("requires API Client ID but default/empty one set")
errBase64DecodeFailure = errors.New("base64 decode has failed")
errMissingInfo = errors.New("cannot parse meta data missing information in key value pair")
errInvalidCredentialMetaDataLength = errors.New("invalid meta data to process credentials")
errContextCredentialsFailure = errors.New("context credentials type assertion failure")
errMetaDataIsNil = errors.New("meta data is nil")
errRequiresAPIKey = errors.New("requires API key but default/empty one set")
errRequiresAPISecret = errors.New("requires API secret but default/empty one set")
errRequiresAPIPEMKey = errors.New("requires API PEM key but default/empty one set")
errRequiresAPIClientID = errors.New("requires API Client ID but default/empty one set")
errBase64DecodeFailure = errors.New("base64 decode has failed")
errContextCredentialsFailure = errors.New("context credentials type assertion failure")
)
// ParseCredentialsMetadata intercepts and converts credentials metadata to a
// static type for authentication processing and protection.
func ParseCredentialsMetadata(ctx context.Context, md metadata.MD) (context.Context, error) {
if md == nil {
return ctx, errMetaDataIsNil
}
credMD, ok := md[string(contextCredentialsFlag)]
if !ok || len(credMD) == 0 {
return ctx, nil
}
if len(credMD) != 1 {
return ctx, errInvalidCredentialMetaDataLength
}
segregatedCreds := strings.Split(credMD[0], ",")
var ctxCreds Credentials
var subAccountHere string
for x := range segregatedCreds {
keyVals := strings.Split(segregatedCreds[x], ":")
if len(keyVals) != 2 {
return ctx, fmt.Errorf("%w received %v fields, expected 2 contains: %s",
errMissingInfo,
len(keyVals),
keyVals)
}
switch keyVals[0] {
case key:
ctxCreds.Key = keyVals[1]
case secret:
ctxCreds.Secret = keyVals[1]
case subAccount:
// Capture sub account as this can override if other values are
// not included in metadata.
subAccountHere = keyVals[1]
case clientID:
ctxCreds.ClientID = keyVals[1]
case _PEMKey:
ctxCreds.PEMKey = keyVals[1]
case oneTimePassword:
ctxCreds.OneTimePassword = keyVals[1]
}
}
if ctxCreds.IsEmpty() && subAccountHere != "" {
// This will override default sub account details if needed.
return deploySubAccountOverrideToContext(ctx, subAccountHere), nil
}
// merge sub account to main context credentials
ctxCreds.SubAccount = subAccountHere
return DeployCredentialsToContext(ctx, &ctxCreds), nil
}
// Credentials define parameters that allow for an authenticated request.
type Credentials struct {
Key string
Secret string
ClientID string
PEMKey string
SubAccount string
OneTimePassword string
}
// DeployCredentialsToContext sets credentials for internal use to context which
// can override default credential values.
func DeployCredentialsToContext(ctx context.Context, creds *Credentials) context.Context {
flag, store := creds.getInternal()
return context.WithValue(ctx, flag, store)
}
// deploySubAccountOverrideToContext sets subaccount as override to credentials
// as a separate flag.
func deploySubAccountOverrideToContext(ctx context.Context, subAccount string) context.Context {
return context.WithValue(ctx, contextSubAccountFlag, subAccount)
}
// GetMetaData returns the credentials for metadata context deployment
func (c *Credentials) GetMetaData() (flag, values string) {
vals := make([]string, 0, 6)
if c.Key != "" {
vals = append(vals, key+":"+c.Key)
}
if c.Secret != "" {
vals = append(vals, secret+":"+c.Secret)
}
if c.SubAccount != "" {
vals = append(vals, subAccount+":"+c.SubAccount)
}
if c.ClientID != "" {
vals = append(vals, clientID+":"+c.ClientID)
}
if c.PEMKey != "" {
vals = append(vals, _PEMKey+":"+c.PEMKey)
}
if c.OneTimePassword != "" {
vals = append(vals, oneTimePassword+":"+c.OneTimePassword)
}
return string(contextCredentialsFlag), strings.Join(vals, ",")
}
// IsEmpty return true if the underlying credentials type has not been filled
// with at least one item.
func (c *Credentials) IsEmpty() bool {
return c == nil || c.ClientID == "" &&
c.Key == "" &&
c.OneTimePassword == "" &&
c.PEMKey == "" &&
c.Secret == "" &&
c.SubAccount == ""
}
// contextCredentialsStore protects the stored credentials for use in a context
type contextCredentialsStore struct {
creds *Credentials
mu sync.RWMutex
}
// getInternal returns the values for assignment to an internal context
func (c *Credentials) getInternal() (contextCredential, *contextCredentialsStore) {
if c.IsEmpty() {
return "", nil
}
store := &contextCredentialsStore{}
store.Load(c)
return contextCredentialsFlag, store
}
// Load stores provided credentials
func (c *contextCredentialsStore) Load(creds *Credentials) {
// Segregate from external call
cpy := *creds
c.mu.Lock()
c.creds = &cpy
c.mu.Unlock()
}
// Get returns the full credentials from the store
func (c *contextCredentialsStore) Get() *Credentials {
c.mu.RLock()
creds := *c.creds
c.mu.RUnlock()
return &creds
}
// SetKey sets new key for the default credentials
func (a *API) SetKey(key string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
a.credentials = &account.Credentials{}
}
a.credentials.Key = key
}
@@ -209,7 +45,7 @@ func (a *API) SetSecret(secret string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
a.credentials = &account.Credentials{}
}
a.credentials.Secret = secret
}
@@ -219,7 +55,7 @@ func (a *API) SetClientID(clientID string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
a.credentials = &account.Credentials{}
}
a.credentials.ClientID = clientID
}
@@ -229,7 +65,7 @@ func (a *API) SetPEMKey(pem string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
a.credentials = &account.Credentials{}
}
a.credentials.PEMKey = pem
}
@@ -239,14 +75,14 @@ func (a *API) SetSubAccount(sub string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
a.credentials = &account.Credentials{}
}
a.credentials.SubAccount = sub
}
// CheckCredentials checks to see if the required fields have been set before
// sending an authenticated API request
func (b *Base) CheckCredentials(creds *Credentials, isContext bool) error {
func (b *Base) CheckCredentials(creds *account.Credentials, isContext bool) error {
if b.SkipAuthCheck {
return nil
}
@@ -277,7 +113,7 @@ func (b *Base) AreCredentialsValid(ctx context.Context) bool {
// GetDefaultCredentials returns the exchange.Base api credentials loaded by
// config.json
func (b *Base) GetDefaultCredentials() *Credentials {
func (b *Base) GetDefaultCredentials() *account.Credentials {
b.API.credMu.RLock()
defer b.API.credMu.RUnlock()
if b.API.credentials == nil {
@@ -289,12 +125,14 @@ func (b *Base) GetDefaultCredentials() *Credentials {
// GetCredentials checks and validates current credentials, context credentials
// override default credentials, if no credentials found, will return an error.
func (b *Base) GetCredentials(ctx context.Context) (*Credentials, error) {
value := ctx.Value(contextCredentialsFlag)
func (b *Base) GetCredentials(ctx context.Context) (*account.Credentials, error) {
value := ctx.Value(account.ContextCredentialsFlag)
if value != nil {
ctxCredStore, ok := value.(*contextCredentialsStore)
ctxCredStore, ok := value.(*account.ContextCredentialsStore)
if !ok {
return &Credentials{}, errContextCredentialsFailure
// NOTE: Return empty credentials on error to limit panic on
// websocket handling.
return &account.Credentials{}, errContextCredentialsFailure
}
creds := ctxCredStore.Get()
@@ -306,9 +144,11 @@ func (b *Base) GetCredentials(ctx context.Context) (*Credentials, error) {
err := b.CheckCredentials(b.API.credentials, false)
if err != nil {
return &Credentials{}, err
// NOTE: Return empty credentials on error to limit panic on websocket
// handling.
return &account.Credentials{}, err
}
subAccountOverride, ok := ctx.Value(contextSubAccountFlag).(string)
subAccountOverride, ok := ctx.Value(account.ContextSubAccountFlag).(string)
b.API.credMu.RLock()
defer b.API.credMu.RUnlock()
creds := *b.API.credentials
@@ -319,7 +159,7 @@ func (b *Base) GetCredentials(ctx context.Context) (*Credentials, error) {
}
// ValidateAPICredentials validates the exchanges API credentials
func (b *Base) ValidateAPICredentials(creds *Credentials) error {
func (b *Base) ValidateAPICredentials(creds *account.Credentials) error {
b.API.credMu.RLock()
defer b.API.credMu.RUnlock()
if creds.IsEmpty() {
@@ -359,7 +199,7 @@ func (b *Base) SetCredentials(apiKey, apiSecret, clientID, subaccount, pemKey, o
b.API.credMu.Lock()
defer b.API.credMu.Unlock()
if b.API.credentials == nil {
b.API.credentials = &Credentials{}
b.API.credentials = &account.Credentials{}
}
b.API.credentials.Key = apiKey
b.API.credentials.ClientID = clientID

View File

@@ -6,97 +6,9 @@ import (
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
"google.golang.org/grpc/metadata"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
)
func TestParseCredentialsMetadata(t *testing.T) {
t.Parallel()
_, err := ParseCredentialsMetadata(context.Background(), nil)
if !errors.Is(err, errMetaDataIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errMetaDataIsNil)
}
_, err = ParseCredentialsMetadata(context.Background(), metadata.MD{})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
ctx := metadata.AppendToOutgoingContext(context.Background(),
string(contextCredentialsFlag), "wow", string(contextCredentialsFlag), "wow2")
nortyMD, _ := metadata.FromOutgoingContext(ctx)
_, err = ParseCredentialsMetadata(context.Background(), nortyMD)
if !errors.Is(err, errInvalidCredentialMetaDataLength) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCredentialMetaDataLength)
}
ctx = metadata.AppendToOutgoingContext(context.Background(),
string(contextCredentialsFlag), "brokenstring")
nortyMD, _ = metadata.FromOutgoingContext(ctx)
_, err = ParseCredentialsMetadata(context.Background(), nortyMD)
if !errors.Is(err, errMissingInfo) {
t.Fatalf("received: '%v' but expected: '%v'", err, errMissingInfo)
}
beforeCreds := Credentials{
Key: "superkey",
Secret: "supersecret",
SubAccount: "supersub",
ClientID: "superclient",
PEMKey: "superpem",
OneTimePassword: "superOneTimePasssssss",
}
flag, outGoing := beforeCreds.GetMetaData()
ctx = metadata.AppendToOutgoingContext(context.Background(), flag, outGoing)
lovelyMD, _ := metadata.FromOutgoingContext(ctx)
ctx, err = ParseCredentialsMetadata(context.Background(), lovelyMD)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
store, ok := ctx.Value(contextCredentialsFlag).(*contextCredentialsStore)
if !ok {
t.Fatal("should have processed")
}
afterCreds := store.Get()
if afterCreds.Key != "superkey" &&
afterCreds.Secret != "supersecret" &&
afterCreds.SubAccount != "supersub" &&
afterCreds.ClientID != "superclient" &&
afterCreds.PEMKey != "superpem" &&
afterCreds.OneTimePassword != "superOneTimePasssssss" {
t.Fatal("unexpected values")
}
// subaccount override
subaccount := Credentials{
SubAccount: "supersub",
}
flag, outGoing = subaccount.GetMetaData()
ctx = metadata.AppendToOutgoingContext(context.Background(), flag, outGoing)
lovelyMD, _ = metadata.FromOutgoingContext(ctx)
ctx, err = ParseCredentialsMetadata(context.Background(), lovelyMD)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
sa, ok := ctx.Value(contextSubAccountFlag).(string)
if !ok {
t.Fatal("should have processed")
}
if sa != "supersub" {
t.Fatal("unexpected value")
}
}
func TestGetCredentials(t *testing.T) {
t.Parallel()
var b Base
@@ -106,26 +18,26 @@ func TestGetCredentials(t *testing.T) {
}
b.API.CredentialsValidator.RequiresKey = true
ctx := DeployCredentialsToContext(context.Background(), &Credentials{Secret: "wow"})
ctx := account.DeployCredentialsToContext(context.Background(), &account.Credentials{Secret: "wow"})
_, err = b.GetCredentials(ctx)
if !errors.Is(err, errRequiresAPIKey) {
t.Fatalf("received: %v but expected: %v", err, errRequiresAPIKey)
}
b.API.CredentialsValidator.RequiresSecret = true
ctx = DeployCredentialsToContext(context.Background(), &Credentials{Key: "wow"})
ctx = account.DeployCredentialsToContext(context.Background(), &account.Credentials{Key: "wow"})
_, err = b.GetCredentials(ctx)
if !errors.Is(err, errRequiresAPISecret) {
t.Fatalf("received: %v but expected: %v", err, errRequiresAPISecret)
}
ctx = context.WithValue(context.Background(), contextCredentialsFlag, "pewpew")
ctx = context.WithValue(context.Background(), account.ContextCredentialsFlag, "pewpew")
_, err = b.GetCredentials(ctx)
if !errors.Is(err, errContextCredentialsFailure) {
t.Fatalf("received: %v but expected: %v", err, errContextCredentialsFailure)
}
fullCred := Credentials{
fullCred := &account.Credentials{
Key: "superkey",
Secret: "supersecret",
SubAccount: "supersub",
@@ -134,9 +46,7 @@ func TestGetCredentials(t *testing.T) {
OneTimePassword: "superOneTimePasssssss",
}
flag, store := fullCred.getInternal()
ctx = context.WithValue(context.Background(), flag, store)
ctx = account.DeployCredentialsToContext(context.Background(), fullCred)
creds, err := b.GetCredentials(ctx)
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
@@ -151,7 +61,7 @@ func TestGetCredentials(t *testing.T) {
t.Fatal("unexpected values")
}
lonelyCred := Credentials{
lonelyCred := &account.Credentials{
Key: "superkey",
Secret: "supersecret",
SubAccount: "supersub",
@@ -159,9 +69,7 @@ func TestGetCredentials(t *testing.T) {
OneTimePassword: "superOneTimePasssssss",
}
flag, store = lonelyCred.getInternal()
ctx = context.WithValue(context.Background(), flag, store)
ctx = account.DeployCredentialsToContext(context.Background(), lonelyCred)
b.API.CredentialsValidator.RequiresClientID = true
_, err = b.GetCredentials(ctx)
if !errors.Is(err, errRequiresAPIClientID) {
@@ -171,7 +79,8 @@ func TestGetCredentials(t *testing.T) {
b.API.SetKey("hello")
b.API.SetSecret("sir")
b.API.SetClientID("1337")
ctx = deploySubAccountOverrideToContext(context.Background(), "superaccount")
ctx = context.WithValue(context.Background(), account.ContextSubAccountFlag, "superaccount")
overridedSA, err := b.GetCredentials(ctx)
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
@@ -203,7 +112,7 @@ func TestAreCredentialsValid(t *testing.T) {
if b.AreCredentialsValid(context.Background()) {
t.Fatal("should not be valid")
}
ctx := DeployCredentialsToContext(context.Background(), &Credentials{Key: "hello"})
ctx := account.DeployCredentialsToContext(context.Background(), &account.Credentials{Key: "hello"})
if !b.AreCredentialsValid(ctx) {
t.Fatal("should be valid")
}
@@ -277,11 +186,11 @@ func TestCheckCredentials(t *testing.T) {
b := Base{
SkipAuthCheck: true,
API: API{credentials: &Credentials{}},
API: API{credentials: &account.Credentials{}},
}
// Test SkipAuthCheck
err := b.CheckCredentials(&Credentials{}, false)
err := b.CheckCredentials(&account.Credentials{}, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -322,56 +231,35 @@ func TestCheckCredentials(t *testing.T) {
}
}
func TestGetInternal(t *testing.T) {
t.Parallel()
flag, store := (&Credentials{}).getInternal()
if flag != "" {
t.Fatal("unexpected value")
}
if store != nil {
t.Fatal("unexpected value")
}
flag, store = (&Credentials{Key: "wow"}).getInternal()
if flag != contextCredentialsFlag {
t.Fatal("unexpected value")
}
if store == nil {
t.Fatal("unexpected value")
}
if store.Get().Key != "wow" {
t.Fatal("unexpected value")
}
}
func TestAPISetters(t *testing.T) {
t.Parallel()
api := API{}
api.SetKey(key)
if api.credentials.Key != key {
api.SetKey(account.Key)
if api.credentials.Key != account.Key {
t.Fatal("unexpected value")
}
api = API{}
api.SetSecret(secret)
if api.credentials.Secret != secret {
api.SetSecret(account.Secret)
if api.credentials.Secret != account.Secret {
t.Fatal("unexpected value")
}
api = API{}
api.SetClientID((clientID))
if api.credentials.ClientID != clientID {
api.SetClientID(account.ClientID)
if api.credentials.ClientID != account.ClientID {
t.Fatal("unexpected value")
}
api = API{}
api.SetPEMKey(_PEMKey)
if api.credentials.PEMKey != _PEMKey {
api.SetPEMKey(account.PEMKey)
if api.credentials.PEMKey != account.PEMKey {
t.Fatal("unexpected value")
}
api = API{}
api.SetSubAccount(subAccount)
if api.credentials.SubAccount != subAccount {
api.SetSubAccount(account.SubAccountSTR)
if api.credentials.SubAccount != account.SubAccountSTR {
t.Fatal("unexpected value")
}
}
@@ -471,19 +359,3 @@ func TestGetAuthenticatedAPISupport(t *testing.T) {
t.Fatal("Expected WebsocketAuthentication to return true")
}
}
func TestIsEmpty(t *testing.T) {
var c *Credentials
if !c.IsEmpty() {
t.Fatalf("expected: %v but received: %v", true, c.IsEmpty())
}
c = new(Credentials)
if !c.IsEmpty() {
t.Fatalf("expected: %v but received: %v", true, c.IsEmpty())
}
c.SubAccount = "woow"
if c.IsEmpty() {
t.Fatalf("expected: %v but received: %v", false, c.IsEmpty())
}
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -470,7 +471,7 @@ func (b *Base) SetupDefaults(exch *config.Exchange) error {
b.API.AuthenticatedSupport = exch.API.AuthenticatedSupport
b.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport
if b.API.credentials == nil {
b.API.credentials = &Credentials{}
b.API.credentials = &account.Credentials{}
}
b.API.credentials.SubAccount = exch.API.Credentials.Subaccount
if b.API.AuthenticatedSupport || b.API.AuthenticatedWebsocketSupport {

View File

@@ -6,6 +6,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -186,7 +187,7 @@ type API struct {
Endpoints *Endpoints
credentials *Credentials
credentials *account.Credentials
credMu sync.RWMutex
CredentialsValidator struct {

View File

@@ -383,7 +383,11 @@ func (e *EXMO) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc
Currencies: currencies,
})
err = account.Process(&response)
creds, err := e.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -393,11 +397,14 @@ func (e *EXMO) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc
// FetchAccountInfo retrieves balances for all enabled currencies
func (e *EXMO) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(e.Name, assetType)
creds, err := e.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(e.Name, creds, assetType)
if err != nil {
return e.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -507,7 +507,7 @@ func (f *FTX) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Hold
}
resp.Exchange = f.Name
if err := account.Process(&resp); err != nil {
if err := account.Process(&resp, creds); err != nil {
return account.Holdings{}, err
}
@@ -516,11 +516,14 @@ func (f *FTX) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Hold
// FetchAccountInfo retrieves balances for all enabled currencies
func (f *FTX) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(f.Name, assetType)
creds, err := f.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(f.Name, creds, assetType)
if err != nil {
return f.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -427,7 +427,11 @@ func (g *Gateio) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
}
info.Exchange = g.Name
if err := account.Process(&info); err != nil {
creds, err := g.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
@@ -436,11 +440,14 @@ func (g *Gateio) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
// FetchAccountInfo retrieves balances for all enabled currencies
func (g *Gateio) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(g.Name, assetType)
creds, err := g.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(g.Name, creds, assetType)
if err != nil {
return g.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -333,7 +333,11 @@ func (g *Gemini) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
Currencies: currencies,
})
err = account.Process(&response)
creds, err := g.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -343,11 +347,14 @@ func (g *Gemini) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
// FetchAccountInfo retrieves balances for all enabled currencies
func (g *Gemini) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(g.Name, assetType)
creds, err := g.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(g.Name, creds, assetType)
if err != nil {
return g.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -453,7 +453,11 @@ func (h *HitBTC) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
Currencies: currencies,
})
err = account.Process(&response)
creds, err := h.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -463,7 +467,11 @@ func (h *HitBTC) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
// FetchAccountInfo retrieves balances for all enabled currencies
func (h *HitBTC) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(h.Name, assetType)
creds, err := h.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(h.Name, creds, assetType)
if err != nil {
return h.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -15,7 +15,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
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/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -545,7 +545,7 @@ func (h *HUOBI) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, er
// Subscribe sends a websocket message to receive data from the channel
func (h *HUOBI) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var creds *exchange.Credentials
var creds *account.Credentials
if h.Websocket.CanUseAuthenticatedEndpoints() {
var err error
creds, err = h.GetCredentials(context.TODO())
@@ -585,7 +585,7 @@ func (h *HUOBI) Subscribe(channelsToSubscribe []stream.ChannelSubscription) erro
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HUOBI) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var creds *exchange.Credentials
var creds *account.Credentials
if h.Websocket.CanUseAuthenticatedEndpoints() {
var err error
creds, err = h.GetCredentials(context.TODO())
@@ -623,7 +623,7 @@ func (h *HUOBI) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription)
return nil
}
func (h *HUOBI) wsGenerateSignature(creds *exchange.Credentials, timestamp, endpoint string) ([]byte, error) {
func (h *HUOBI) wsGenerateSignature(creds *account.Credentials, timestamp, endpoint string) ([]byte, error) {
values := url.Values{}
values.Set("AccessKeyId", creds.Key)
values.Set("SignatureMethod", signatureMethod)
@@ -668,7 +668,7 @@ func (h *HUOBI) wsLogin(ctx context.Context) error {
return nil
}
func (h *HUOBI) wsAuthenticatedSubscribe(creds *exchange.Credentials, operation, endpoint, topic string) error {
func (h *HUOBI) wsAuthenticatedSubscribe(creds *account.Credentials, operation, endpoint, topic string) error {
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedSubscriptionRequest{
Op: operation,

View File

@@ -808,7 +808,11 @@ func (h *HUOBI) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
}
acc.AssetType = assetType
info.Accounts = append(info.Accounts, acc)
if err := account.Process(&info); err != nil {
creds, err := h.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return info, err
}
return info, nil
@@ -816,7 +820,11 @@ func (h *HUOBI) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
// FetchAccountInfo retrieves balances for all enabled currencies
func (h *HUOBI) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(h.Name, assetType)
creds, err := h.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(h.Name, creds, assetType)
if err != nil {
return h.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -85,7 +85,7 @@ type IBotExchange interface {
UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error
AccountManagement
GetCredentials(ctx context.Context) (*Credentials, error)
GetCredentials(ctx context.Context) (*account.Credentials, error)
ValidateCredentials(ctx context.Context, a asset.Item) error
FunctionalityChecker

View File

@@ -302,7 +302,11 @@ func (i *ItBit) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
Currencies: fullBalance,
})
err = account.Process(&info)
creds, err := i.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&info, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -312,11 +316,14 @@ func (i *ItBit) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
// FetchAccountInfo retrieves balances for all enabled currencies
func (i *ItBit) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(i.Name, assetType)
creds, err := i.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(i.Name, creds, assetType)
if err != nil {
return i.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -630,7 +630,11 @@ func (k *Kraken) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
})
}
}
if err := account.Process(&info); err != nil {
creds, err := k.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
return info, nil
@@ -638,7 +642,11 @@ func (k *Kraken) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a
// FetchAccountInfo retrieves balances for all enabled currencies
func (k *Kraken) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(k.Name, assetType)
creds, err := k.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(k.Name, creds, assetType)
if err != nil {
return k.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -310,7 +311,8 @@ func TestLoadPrivKey(t *testing.T) {
t.Error(err)
}
ctx := exchange.DeployCredentialsToContext(context.Background(), &exchange.Credentials{Secret: "errortest"})
ctx := account.DeployCredentialsToContext(context.Background(),
&account.Credentials{Secret: "errortest"})
err = l.loadPrivKey(ctx)
if err == nil {
t.Errorf("Expected error due to pemblock nil")

View File

@@ -353,7 +353,11 @@ func (l *Lbank) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
info.Accounts = append(info.Accounts, acc)
info.Exchange = l.Name
err = account.Process(&info)
creds, err := l.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&info, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -362,7 +366,11 @@ func (l *Lbank) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
// FetchAccountInfo retrieves balances for all enabled currencies
func (l *Lbank) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(l.Name, assetType)
creds, err := l.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(l.Name, creds, assetType)
if err != nil {
return l.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -300,7 +300,11 @@ func (l *LocalBitcoins) UpdateAccountInfo(ctx context.Context, assetType asset.I
}},
})
err = account.Process(&response)
creds, err := l.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -310,11 +314,14 @@ func (l *LocalBitcoins) UpdateAccountInfo(ctx context.Context, assetType asset.I
// FetchAccountInfo retrieves balances for all enabled currencies
func (l *LocalBitcoins) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(l.Name, assetType)
creds, err := l.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(l.Name, creds, assetType)
if err != nil {
return l.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/google/go-querystring/query"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
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/request"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -585,7 +586,7 @@ func (o *OKGroup) SendHTTPRequest(ctx context.Context, ep exchange.URL, httpMeth
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
if authenticated {
var creds *exchange.Credentials
var creds *account.Credentials
creds, err = o.GetCredentials(ctx)
if err != nil {
return nil, err

View File

@@ -216,7 +216,11 @@ func (o *OKGroup) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
resp.Accounts = append(resp.Accounts, currencyAccount)
err = account.Process(&resp)
creds, err := o.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&resp, creds)
if err != nil {
return resp, err
}
@@ -226,11 +230,14 @@ func (o *OKGroup) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
// FetchAccountInfo retrieves balances for all enabled currencies
func (o *OKGroup) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(o.Name, assetType)
creds, err := o.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(o.Name, creds, assetType)
if err != nil {
return o.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
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/order"
@@ -542,7 +541,7 @@ func (p *Poloniex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription,
// Subscribe sends a websocket message to receive data from the channel
func (p *Poloniex) Subscribe(sub []stream.ChannelSubscription) error {
var creds *exchange.Credentials
var creds *account.Credentials
if p.IsWebsocketAuthenticationSupported() {
var err error
creds, err = p.GetCredentials(context.TODO())
@@ -589,7 +588,7 @@ channels:
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (p *Poloniex) Unsubscribe(unsub []stream.ChannelSubscription) error {
var creds *exchange.Credentials
var creds *account.Credentials
if p.IsWebsocketAuthenticationSupported() {
var err error
creds, err = p.GetCredentials(context.TODO())

View File

@@ -427,7 +427,11 @@ func (p *Poloniex) UpdateAccountInfo(ctx context.Context, assetType asset.Item)
Currencies: currencies,
})
err = account.Process(&response)
creds, err := p.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -437,7 +441,11 @@ func (p *Poloniex) UpdateAccountInfo(ctx context.Context, assetType asset.Item)
// FetchAccountInfo retrieves balances for all enabled currencies
func (p *Poloniex) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(p.Name, assetType)
creds, err := p.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(p.Name, creds, assetType)
if err != nil {
return p.UpdateAccountInfo(ctx, assetType)
}

View File

@@ -328,7 +328,11 @@ func (y *Yobit) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
Currencies: currencies,
})
err = account.Process(&response)
creds, err := y.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
@@ -338,11 +342,14 @@ func (y *Yobit) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac
// FetchAccountInfo retrieves balances for all enabled currencies
func (y *Yobit) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(y.Name, assetType)
creds, err := y.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(y.Name, creds, assetType)
if err != nil {
return y.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}

View File

@@ -397,7 +397,11 @@ func (z *ZB) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (accou
Currencies: balances,
})
if err := account.Process(&info); err != nil {
creds, err := z.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
@@ -406,11 +410,14 @@ func (z *ZB) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (accou
// FetchAccountInfo retrieves balances for all enabled currencies
func (z *ZB) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
acc, err := account.GetHoldings(z.Name, assetType)
creds, err := z.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(z.Name, creds, assetType)
if err != nil {
return z.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}