diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 8876284b5a..0f4a8b457a 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -213,6 +213,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/keystores", handlers.getKeystores).Methods("GET") getAPIRouterNoError(apiRouter)("/accounts", handlers.getAccounts).Methods("GET") getAPIRouter(apiRouter)("/accounts/balance", handlers.getAccountsBalance).Methods("GET") + getAPIRouter(apiRouter)("/accounts/coins-balance", handlers.getCoinsTotalBalance).Methods("GET") getAPIRouter(apiRouter)("/accounts/total-balance", handlers.getAccountsTotalBalance).Methods("GET") getAPIRouterNoError(apiRouter)("/set-account-active", handlers.postSetAccountActive).Methods("POST") getAPIRouterNoError(apiRouter)("/set-token-active", handlers.postSetTokenActive).Methods("POST") @@ -749,6 +750,59 @@ func (handlers *Handlers) getAccountsBalance(*http.Request) (interface{}, error) return totalAmount, nil } +// getCoinsTotalBalance returns the total balances grouped by coins. +func (handlers *Handlers) getCoinsTotalBalance(_ *http.Request) (interface{}, error) { + totalPerCoin := make(map[coin.Code]*big.Int) + conversionsPerCoin := make(map[coin.Code]map[string]string) + + totalAmount := make(map[coin.Code]accountHandlers.FormattedAmount) + + for _, account := range handlers.backend.Accounts() { + if account.Config().Config.Inactive || account.Config().Config.HiddenBecauseUnused { + continue + } + if account.FatalError() { + continue + } + err := account.Initialize() + if err != nil { + return nil, err + } + coinCode := account.Coin().Code() + b, err := account.Balance() + if err != nil { + return nil, err + } + amount := b.Available() + if _, ok := totalPerCoin[coinCode]; !ok { + totalPerCoin[coinCode] = amount.BigInt() + + } else { + totalPerCoin[coinCode] = new(big.Int).Add(totalPerCoin[coinCode], amount.BigInt()) + } + + conversionsPerCoin[coinCode] = coin.Conversions( + coin.NewAmount(totalPerCoin[coinCode]), + account.Coin(), + false, + account.Config().RateUpdater, + util.FormatBtcAsSat(handlers.backend.Config().AppConfig().Backend.BtcUnit)) + } + + for k, v := range totalPerCoin { + currentCoin, err := handlers.backend.Coin(k) + if err != nil { + return nil, err + } + totalAmount[k] = accountHandlers.FormattedAmount{ + Amount: currentCoin.FormatAmount(coin.NewAmount(v), false), + Unit: currentCoin.GetFormatUnit(false), + Conversions: conversionsPerCoin[k], + } + } + return totalAmount, nil +} + // getAccountsTotalBalanceHandler returns the total balance of all the accounts, gruped by keystore. func (handlers *Handlers) getAccountsTotalBalance(*http.Request) (interface{}, error) { type response struct { diff --git a/frontends/web/src/api/account.ts b/frontends/web/src/api/account.ts index 5661394243..ffe24e82d3 100644 --- a/frontends/web/src/api/account.ts +++ b/frontends/web/src/api/account.ts @@ -103,6 +103,14 @@ export const getAccountsTotalBalance = (): Promise => { + return apiGet('accounts/coins-balance'); +}; + type TEthAccountCodeAndNameByAddress = SuccessResponse & { code: AccountCode; name: string; diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index e232ccd161..636470171e 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -41,6 +41,7 @@ "accountSummary": { "availableBalance": "Available balance", "balance": "Balance", + "coin": "Coin", "exportSummary": "Export accounts summary to downloads folder as CSV file", "fiatBalance": "Fiat balance", "name": "Account name", diff --git a/frontends/web/src/routes/account/summary/accountssummary.tsx b/frontends/web/src/routes/account/summary/accountssummary.tsx index ea9513594c..7d0c48f7e7 100644 --- a/frontends/web/src/routes/account/summary/accountssummary.tsx +++ b/frontends/web/src/routes/account/summary/accountssummary.tsx @@ -28,6 +28,7 @@ import { GuideWrapper, GuidedContent, Header, Main } from '../../../components/l import { View } from '../../../components/view/view'; import { Chart } from './chart'; import { SummaryBalance } from './summarybalance'; +import { CoinBalance } from './coinbalance'; import { AddBuyReceiveOnEmptyBalances } from '../info/buyReceiveCTA'; import { Entry } from '../../../components/guide/entry'; import { Guide } from '../../../components/guide/guide'; @@ -60,6 +61,7 @@ export function AccountsSummary({ const [summaryData, setSummaryData] = useState(); const [balancePerCoin, setBalancePerCoin] = useState(); const [accountsTotalBalance, setAccountsTotalBalance] = useState(); + const [coinsTotalBalance, setCoinsTotalBalance] = useState(); const [balances, setBalances] = useState(); const hasCard = useSDCard(devices); @@ -109,6 +111,17 @@ export function AccountsSummary({ } }, [mounted]); + const getCoinsTotalBalance = useCallback(async () => { + try { + const coinBalance = await accountApi.getCoinsTotalBalance(); + if (!mounted.current) { + return; + } + setCoinsTotalBalance(coinBalance); + } catch (err) { + console.error(err); + } + }, [mounted]); const onStatusChanged = useCallback(async ( code: accountApi.AccountCode, @@ -157,7 +170,8 @@ export function AccountsSummary({ getAccountSummary(); getAccountsBalance(); getAccountsTotalBalance(); - }, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, defaultCurrency]); + getCoinsTotalBalance(); + }, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, getCoinsTotalBalance, defaultCurrency]); // update the timer to get a new account summary update when receiving the previous call result. useEffect(() => { @@ -177,8 +191,8 @@ export function AccountsSummary({ onStatusChanged(account.code); }); getAccountsBalance(); - }, [onStatusChanged, getAccountsBalance, accounts]); - + getCoinsTotalBalance(); + }, [onStatusChanged, getAccountsBalance, getCoinsTotalBalance, accounts]); return ( @@ -198,6 +212,13 @@ export function AccountsSummary({ ) : undefined } /> + {accountsByKeystore.length > 1 && ( + + )} {accountsByKeystore && (accountsByKeystore.map(({ keystore, accounts }) => { + return accounts.reduce((accountPerCoin, account) => { + accountPerCoin[account.coinCode] + ? accountPerCoin[account.coinCode].push(account) + : accountPerCoin[account.coinCode] = [account]; + return accountPerCoin; + }, {} as TAccountCoinMap); + }; + + const accountsPerCoin = getAccountsPerCoin(); + const coins = Object.keys(accountsPerCoin) as accountApi.CoinCode[]; + + return ( +
+
+

{t('accountSummary.total')}

+
+
+ + + + + + + + + + + + + + + { accounts.length > 0 ? ( + coins.map(coinCode => { + if (accountsPerCoin[coinCode]?.length >= 1) { + const account = accountsPerCoin[coinCode][0]; + return ( + + ); + } + return null; + })) : null} + + + + + + + +
{t('accountSummary.coin')}{t('accountSummary.balance')}{t('accountSummary.fiatBalance')}
+ {t('accountSummary.total')} + + {(summaryData && summaryData.formattedChartTotal !== null) ? ( + <> + + + + {' '} + + {summaryData.chartFiat} + + + ) : () } +
+
+
+ ); +} \ No newline at end of file diff --git a/frontends/web/src/routes/account/summary/subtotalrow.tsx b/frontends/web/src/routes/account/summary/subtotalrow.tsx index 12e3c44dfa..fcbaf562e3 100644 --- a/frontends/web/src/routes/account/summary/subtotalrow.tsx +++ b/frontends/web/src/routes/account/summary/subtotalrow.tsx @@ -65,3 +65,36 @@ export function SubTotalRow ({ coinCode, coinName, balance }: TProps) { ); } + + +export function SubTotalCoinRow ({ coinCode, coinName, balance }: TProps) { + const { t } = useTranslation(); + const nameCol = ( + +
+ + + {coinName} + +
+ + ); + if (!balance) { + return null; + } + return ( + + { nameCol } + + + + {' '} + {balance.unit} + + + + + + + ); +}