Skip to content

Commit 48170bb

Browse files
committed
Merge branch 'accounts-summary'
2 parents 8eb65b1 + c3a63c2 commit 48170bb

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
@@ -610,14 +610,6 @@ func (backend *Backend) Accounts() AccountsList {
610610
return slices.Clone(backend.accounts)
611611
}
612612

613-
// KeystoreTotalAmount represents the total balance amount of the accounts belonging to a keystore.
614-
type KeystoreTotalAmount = struct {
615-
// FiatUnit is the fiat unit of the total amount
616-
FiatUnit string `json:"fiatUnit"`
617-
// Total formatted for frontend visualization
618-
Total string `json:"total"`
619-
}
620-
621613
// OnAccountInit installs a callback to be called when an account is initialized.
622614
func (backend *Backend) OnAccountInit(f func(accounts.Interface)) {
623615
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)