Skip to content

Commit c3a63c2

Browse files
committed
backend/accounts: refactor accounts summary endpoint and functions
The code needed to load was the accounts summary table was complicated and fragmented. This refactors some parts of the code, unifying some endpoints and adding unit tests.
1 parent af10b16 commit c3a63c2

File tree

11 files changed

+299
-295
lines changed

11 files changed

+299
-295
lines changed

backend/accounts.go

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -268,19 +268,95 @@ func (backend *Backend) accountFiatBalance(account accounts.Interface, fiat stri
268268
return fiatValue, nil
269269
}
270270

271-
// AccountsTotalBalanceByKeystore returns a map of accounts' total balances across coins, grouped by keystore.
272-
func (backend *Backend) AccountsTotalBalanceByKeystore() (map[string]KeystoreTotalAmount, error) {
273-
totalAmounts := make(map[string]KeystoreTotalAmount)
274-
fiat := backend.Config().AppConfig().Backend.MainFiat
271+
type coinFormattedAmount struct {
272+
CoinCode coinpkg.Code `json:"coinCode"`
273+
CoinName string `json:"coinName"`
274+
FormattedAmount coinpkg.FormattedAmountWithConversions `json:"formattedAmount"`
275+
}
276+
277+
// getCoinsTotalBalance returns the total balances grouped by coins.
278+
func (backend *Backend) coinsTotalBalance() ([]coinFormattedAmount, error) {
279+
var coinFormattedAmounts []coinFormattedAmount
280+
var sortedCoins []coinpkg.Code
281+
totalCoinsBalances := make(map[coinpkg.Code]*big.Int)
282+
283+
for _, account := range backend.Accounts() {
284+
if account.Config().Config.Inactive || account.Config().Config.HiddenBecauseUnused {
285+
continue
286+
}
287+
if account.FatalError() {
288+
continue
289+
}
290+
err := account.Initialize()
291+
if err != nil {
292+
return nil, err
293+
}
294+
coinCode := account.Coin().Code()
295+
b, err := account.Balance()
296+
if err != nil {
297+
return nil, err
298+
}
299+
amount := b.Available()
300+
301+
if totalBalance, exists := totalCoinsBalances[coinCode]; exists {
302+
totalBalance.Add(totalBalance, amount.BigInt())
303+
} else {
304+
totalCoinsBalances[coinCode] = amount.BigInt()
305+
sortedCoins = append(sortedCoins, coinCode)
306+
}
307+
}
308+
309+
for _, coinCode := range sortedCoins {
310+
coin, err := backend.Coin(coinCode)
311+
if err != nil {
312+
return nil, err
313+
}
314+
coinAmount := coinpkg.NewAmount(totalCoinsBalances[coinCode])
315+
coinFormattedAmounts = append(coinFormattedAmounts, coinFormattedAmount{
316+
CoinCode: coinCode,
317+
CoinName: coin.Name(),
318+
FormattedAmount: coinpkg.FormattedAmountWithConversions{
319+
Amount: coin.FormatAmount(coinAmount, false),
320+
Unit: coin.GetFormatUnit(false),
321+
Conversions: coinpkg.Conversions(
322+
coinAmount,
323+
coin,
324+
false,
325+
backend.RatesUpdater(),
326+
),
327+
},
328+
})
329+
}
330+
return coinFormattedAmounts, nil
331+
}
332+
333+
// AmountsByCoin maps the total amount of each coin.
334+
type AmountsByCoin map[coinpkg.Code]coinpkg.FormattedAmountWithConversions
335+
336+
// KeystoreBalance represents the total balance amount of the accounts belonging to a keystore.
337+
type KeystoreBalance = struct {
338+
// FiatUnit is the fiat unit of the balance
339+
FiatUnit string `json:"fiatUnit"`
340+
// Fiat total formatted for frontend visualization
341+
Total string `json:"total"`
342+
// Total amounts for each coin
343+
CoinsBalance AmountsByCoin `json:"coinsBalance"`
344+
}
345+
346+
// keystoresBalance returns a map of accounts' total balances across coins, grouped by keystore.
347+
func (backend *Backend) keystoresBalance() (map[string]KeystoreBalance, error) {
348+
keystoreBalanceMap := make(map[string]KeystoreBalance)
349+
fiatUnit := backend.Config().AppConfig().Backend.MainFiat
275350

276351
accountsByKeystore, err := backend.AccountsByKeystore()
277352
if err != nil {
278353
return nil, err
279354
}
280355
for rootFingerprint, accountList := range accountsByKeystore {
281-
currentTotal := new(big.Rat)
356+
keystoreCoinsBalance := make(map[coinpkg.Code]*big.Int)
357+
keystoreTotalBalance := new(big.Rat)
282358
for _, account := range accountList {
283-
if account.Config().Config.Inactive {
359+
if account.Config().Config.Inactive || account.Config().Config.HiddenBecauseUnused {
284360
continue
285361
}
286362
if account.FatalError() {
@@ -291,18 +367,74 @@ func (backend *Backend) AccountsTotalBalanceByKeystore() (map[string]KeystoreTot
291367
return nil, err
292368
}
293369

294-
fiatValue, err := backend.accountFiatBalance(account, fiat)
370+
accountFiatBalance, err := backend.accountFiatBalance(account, fiatUnit)
371+
if err != nil {
372+
return nil, err
373+
}
374+
keystoreTotalBalance.Add(keystoreTotalBalance, accountFiatBalance)
375+
376+
coinCode := account.Coin().Code()
377+
balance, err := account.Balance()
378+
if err != nil {
379+
return nil, err
380+
}
381+
accountBalance := balance.Available().BigInt()
382+
if _, ok := keystoreCoinsBalance[coinCode]; !ok {
383+
keystoreCoinsBalance[coinCode] = accountBalance
384+
385+
} else {
386+
keystoreCoinsBalance[coinCode] = new(big.Int).Add(keystoreCoinsBalance[coinCode], accountBalance)
387+
}
388+
}
389+
390+
keystoreCoinsAmount := AmountsByCoin{}
391+
for coinCode, coinBalance := range keystoreCoinsBalance {
392+
coinAmount := coinpkg.NewAmount(coinBalance)
393+
coin, err := backend.Coin(coinCode)
295394
if err != nil {
296395
return nil, err
297396
}
298-
currentTotal.Add(currentTotal, fiatValue)
397+
keystoreCoinsAmount[coinCode] = coinpkg.FormattedAmountWithConversions{
398+
Amount: coin.FormatAmount(coinAmount, false),
399+
Unit: coin.GetFormatUnit(false),
400+
Conversions: coinpkg.Conversions(
401+
coinAmount,
402+
coin,
403+
false,
404+
backend.ratesUpdater),
405+
}
299406
}
300-
totalAmounts[rootFingerprint] = KeystoreTotalAmount{
301-
FiatUnit: fiat,
302-
Total: coinpkg.FormatAsCurrency(currentTotal, fiat),
407+
408+
keystoreBalanceMap[rootFingerprint] = KeystoreBalance{
409+
FiatUnit: fiatUnit,
410+
Total: coinpkg.FormatAsCurrency(keystoreTotalBalance, fiatUnit),
411+
CoinsBalance: keystoreCoinsAmount,
303412
}
304413
}
305-
return totalAmounts, nil
414+
return keystoreBalanceMap, nil
415+
}
416+
417+
// AccountsBalanceSummary holds the total balance for each coin and of each keystore.
418+
type AccountsBalanceSummary struct {
419+
KeystoresBalance map[string]KeystoreBalance `json:"keystoresBalance"`
420+
CoinsTotalBalance []coinFormattedAmount `json:"coinsTotalBalance"`
421+
}
422+
423+
// AccountsBalanceSummary returns the total balance for each coin and of each keystore.
424+
func (backend *Backend) AccountsBalanceSummary() (*AccountsBalanceSummary, error) {
425+
keystoresBalance, err := backend.keystoresBalance()
426+
if err != nil {
427+
return nil, err
428+
}
429+
coinsTotalBalance, err := backend.coinsTotalBalance()
430+
if err != nil {
431+
return nil, err
432+
}
433+
434+
return &AccountsBalanceSummary{
435+
KeystoresBalance: keystoresBalance,
436+
CoinsTotalBalance: coinsTotalBalance,
437+
}, nil
306438
}
307439

308440
// LookupInsuredAccounts queries the insurance status of specified or all active BTC accounts

backend/accounts_test.go

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,22 +1447,22 @@ func TestAccountsByKeystore(t *testing.T) {
14471447
require.Nil(t, accountsMap[hex.EncodeToString(ks2Fingerprint)])
14481448
}
14491449

1450-
func TestAccountsTotalBalanceByKeystore(t *testing.T) {
1450+
func TestKeystoresBalance(t *testing.T) {
14511451
b := newBackend(t, testnetDisabled, regtestDisabled)
14521452
defer b.Close()
14531453

14541454
b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, getAddress func(*btc.Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error), log *logrus.Entry) accounts.Interface {
14551455
accountMock := MockBtcAccount(t, config, coin, gapLimits, log)
14561456
accountMock.BalanceFunc = func() (*accounts.Balance, error) {
1457-
return accounts.NewBalance(coinpkg.NewAmountFromInt64(100000), coinpkg.NewAmountFromInt64(0)), nil
1457+
return accounts.NewBalance(coinpkg.NewAmountFromInt64(1e8), coinpkg.NewAmountFromInt64(0)), nil
14581458
}
14591459
return accountMock
14601460
}
14611461

14621462
b.makeEthAccount = func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface {
14631463
accountMock := MockEthAccount(config, coin, httpClient, log)
14641464
accountMock.BalanceFunc = func() (*accounts.Balance, error) {
1465-
return accounts.NewBalance(coinpkg.NewAmountFromInt64(100000), coinpkg.NewAmountFromInt64(0)), nil
1465+
return accounts.NewBalance(coinpkg.NewAmountFromInt64(1e18), coinpkg.NewAmountFromInt64(0)), nil
14661466
}
14671467
return accountMock
14681468
}
@@ -1500,12 +1500,78 @@ func TestAccountsTotalBalanceByKeystore(t *testing.T) {
15001500
b.ratesUpdater = rates.MockRateUpdater()
15011501
defer b.ratesUpdater.Stop()
15021502

1503-
totalBalance, err := b.AccountsTotalBalanceByKeystore()
1503+
keystoresBalance, err := b.keystoresBalance()
15041504
require.NoError(t, err)
15051505

1506-
require.NotNil(t, totalBalance[hex.EncodeToString(ks1Fingerprint)])
1507-
require.Equal(t, "0.02", totalBalance[hex.EncodeToString(ks1Fingerprint)].Total)
1506+
require.NotNil(t, keystoresBalance[hex.EncodeToString(ks1Fingerprint)])
1507+
require.Equal(t, "1.00000000", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].CoinsBalance[coinpkg.CodeBTC].Amount)
1508+
require.Equal(t, "21.00", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].CoinsBalance[coinpkg.CodeBTC].Conversions["USD"])
1509+
require.Equal(t, "1.00000000", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].CoinsBalance[coinpkg.CodeLTC].Amount)
1510+
require.Equal(t, "", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].CoinsBalance[coinpkg.CodeLTC].Conversions["USD"])
1511+
require.Equal(t, "1", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].CoinsBalance[coinpkg.CodeETH].Amount)
1512+
require.Equal(t, "1.00", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].CoinsBalance[coinpkg.CodeETH].Conversions["USD"])
1513+
require.Equal(t, "22.00", keystoresBalance[hex.EncodeToString(ks1Fingerprint)].Total)
1514+
1515+
require.NotNil(t, keystoresBalance[hex.EncodeToString(ks2Fingerprint)])
1516+
require.Equal(t, "22.00", keystoresBalance[hex.EncodeToString(ks2Fingerprint)].Total)
1517+
}
1518+
1519+
func TestCoinsTotalBalance(t *testing.T) {
1520+
b := newBackend(t, testnetDisabled, regtestDisabled)
1521+
defer b.Close()
15081522

1509-
require.NotNil(t, totalBalance[hex.EncodeToString(ks2Fingerprint)])
1510-
require.Equal(t, "0.13", totalBalance[hex.EncodeToString(ks2Fingerprint)].Total)
1523+
b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, getAddress func(*btc.Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error), log *logrus.Entry) accounts.Interface {
1524+
accountMock := MockBtcAccount(t, config, coin, gapLimits, log)
1525+
accountMock.BalanceFunc = func() (*accounts.Balance, error) {
1526+
return accounts.NewBalance(coinpkg.NewAmountFromInt64(1e8), coinpkg.NewAmountFromInt64(0)), nil
1527+
}
1528+
return accountMock
1529+
}
1530+
1531+
b.makeEthAccount = func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface {
1532+
accountMock := MockEthAccount(config, coin, httpClient, log)
1533+
accountMock.BalanceFunc = func() (*accounts.Balance, error) {
1534+
return accounts.NewBalance(coinpkg.NewAmountFromInt64(2e18), coinpkg.NewAmountFromInt64(0)), nil
1535+
}
1536+
return accountMock
1537+
}
1538+
1539+
ks1 := makeBitBox02Multi()
1540+
ks2 := makeBitBox02Multi()
1541+
1542+
ks2.RootFingerprintFunc = keystoreHelper2().RootFingerprint
1543+
ks2.ExtendedPublicKeyFunc = keystoreHelper2().ExtendedPublicKey
1544+
1545+
ks1Fingerprint, err := ks1.RootFingerprint()
1546+
require.NoError(t, err)
1547+
1548+
b.registerKeystore(ks1)
1549+
require.NoError(t, b.SetWatchonly(ks1Fingerprint, true))
1550+
1551+
// Up to 6 hidden accounts for BTC/LTC are added to be scanned even if the accounts are all
1552+
// empty. Calling this function too many times does not add more than that.
1553+
for i := 1; i <= 10; i++ {
1554+
b.maybeAddHiddenUnusedAccounts()
1555+
}
1556+
1557+
b.DeregisterKeystore()
1558+
b.registerKeystore(ks2)
1559+
1560+
for i := 1; i <= 10; i++ {
1561+
b.maybeAddHiddenUnusedAccounts()
1562+
}
1563+
1564+
// This needs to be after all changes in accounts, otherwise it will try to fetch
1565+
// new values and fail.
1566+
b.ratesUpdater = rates.MockRateUpdater()
1567+
defer b.ratesUpdater.Stop()
1568+
1569+
coinsTotalBalance, err := b.coinsTotalBalance()
1570+
require.NoError(t, err)
1571+
require.Equal(t, coinpkg.CodeBTC, coinsTotalBalance[0].CoinCode)
1572+
require.Equal(t, "2.00000000", coinsTotalBalance[0].FormattedAmount.Amount)
1573+
require.Equal(t, coinpkg.CodeLTC, coinsTotalBalance[1].CoinCode)
1574+
require.Equal(t, "2.00000000", coinsTotalBalance[1].FormattedAmount.Amount)
1575+
require.Equal(t, coinpkg.CodeETH, coinsTotalBalance[2].CoinCode)
1576+
require.Equal(t, "4", coinsTotalBalance[2].FormattedAmount.Amount)
15111577
}

backend/backend.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -594,14 +594,6 @@ func (backend *Backend) Accounts() AccountsList {
594594
return slices.Clone(backend.accounts)
595595
}
596596

597-
// KeystoreTotalAmount represents the total balance amount of the accounts belonging to a keystore.
598-
type KeystoreTotalAmount = struct {
599-
// FiatUnit is the fiat unit of the total amount
600-
FiatUnit string `json:"fiatUnit"`
601-
// Total formatted for frontend visualization
602-
Total string `json:"total"`
603-
}
604-
605597
// OnAccountInit installs a callback to be called when an account is initialized.
606598
func (backend *Backend) OnAccountInit(f func(accounts.Interface)) {
607599
backend.onAccountInit = f

backend/coins/coin/amount.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ func (amount Amount) BigInt() *big.Int {
8181

8282
// FormattedAmountWithConversions and json tags.
8383
type FormattedAmountWithConversions struct {
84-
Amount string `json:"amount"`
85-
Unit string `json:"unit"`
86-
Conversions map[string]string `json:"conversions"`
84+
Amount string `json:"amount"`
85+
Unit string `json:"unit"`
86+
Conversions ConversionsMap `json:"conversions"`
8787
// Estimated flag is enabled if the Conversions map was expected to
8888
// be calculated using historical rates, but latest rates have been used instead.
8989
Estimated bool `json:"estimated"`

backend/coins/coin/conversions.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ func FormatAsCurrency(amount *big.Rat, currency string) string {
4747
return formatted
4848
}
4949

50+
// ConversionsMap maps formmatted conversions of a coin amount into fiat currencies.
51+
type ConversionsMap map[string]string
52+
5053
// Conversions handles fiat conversions.
51-
func Conversions(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater) map[string]string {
52-
conversions := map[string]string{}
54+
func Conversions(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater) ConversionsMap {
55+
conversions := ConversionsMap{}
5356
rates := ratesUpdater.LatestPrice()
5457
if rates != nil {
5558
unit := coin.Unit(isFee)
5659

57-
conversions = map[string]string{}
5860
for key, value := range rates[unit] {
5961
convertedAmount := new(big.Rat).Mul(new(big.Rat).SetFloat64(coin.ToUnit(amount, isFee)), new(big.Rat).SetFloat64(value))
6062
conversions[key] = FormatAsCurrency(convertedAmount, key)
@@ -66,14 +68,14 @@ func Conversions(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.Ra
6668
// ConversionsAtTime handles fiat conversions at a specific time.
6769
// It returns the map of conversions and a bool indicating if the rates have been estimated
6870
// using the latest instead of the historical rates for recent transactions.
69-
func ConversionsAtTime(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater, timeStamp *time.Time) (map[string]string, bool) {
71+
func ConversionsAtTime(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater, timeStamp *time.Time) (ConversionsMap, bool) {
7072
latestRatesTime := ratesUpdater.HistoryLatestTimestampCoin(string(coin.Code()))
7173
historicalRatesNotAvailable := latestRatesTime.IsZero() || latestRatesTime.Before(*timeStamp)
7274
if historicalRatesNotAvailable && time.Since(*timeStamp) < 2*time.Hour {
7375
return Conversions(amount, coin, isFee, ratesUpdater), true
7476
}
7577

76-
conversions := map[string]string{}
78+
conversions := ConversionsMap{}
7779
lastRates := ratesUpdater.LatestPrice()
7880
if lastRates != nil {
7981
unit := coin.Unit(isFee)

0 commit comments

Comments
 (0)