diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 8876284b5a..a77468ae51 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 2b20c4e8c1..f63e0b1c99 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -39,6 +39,7 @@ "xpubTypeInfo": "Currently displaying {{scriptType}} extended public key ({{current}} of {{numberOfXPubs}})" }, "accountSummary": { + "coin": "Coin", "availableBalance": "Available balance", "balance": "Balance", "exportSummary": "Export accounts summary to downloads folder as CSV file", diff --git a/frontends/web/src/routes/account/summary/accountssummary.tsx b/frontends/web/src/routes/account/summary/accountssummary.tsx index a060ba5e31..ad0a993849 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'; @@ -58,6 +59,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); @@ -107,6 +109,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, @@ -147,8 +160,9 @@ export function AccountsSummary({ getAccountSummary(); getAccountsBalance(); getAccountsTotalBalance(); + getCoinsTotalBalance(); return () => unsubscribe(subscriptions); - }, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, update]); + }, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, getCoinsTotalBalance, update]); // update the timer to get a new account summary update when receiving the previous call result. useEffect(() => { @@ -168,8 +182,8 @@ export function AccountsSummary({ onStatusChanged(account.code); }); getAccountsBalance(); - }, [onStatusChanged, getAccountsBalance, accounts]); - + getCoinsTotalBalance(); + }, [onStatusChanged, getAccountsBalance, getCoinsTotalBalance, accounts]); return ( @@ -189,6 +203,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 ( +
+ + + + + + + + + + + + + + + { accounts.length > 0 ? ( + coins.map(coinCode => { + if (accountsPerCoin[coinCode]?.length > 1) { + const account = accountsPerCoin[coinCode][0]; + return ( + + ); + } + return ( + + + + ); + })) : ( + + + + )} + + + + + + + +
{t('accountSummary.coin')}{t('accountSummary.balance')}{t('accountSummary.fiatBalance')}
+ {t('accountSummary.noAccount')} +
+ {t('accountSummary.noAccount')} +
+ {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..3c3b801697 100644 --- a/frontends/web/src/routes/account/summary/subtotalrow.tsx +++ b/frontends/web/src/routes/account/summary/subtotalrow.tsx @@ -65,3 +65,39 @@ export function SubTotalRow ({ coinCode, coinName, balance }: TProps) { ); } + + +export function SubTotalCoinRow ({ coinCode, coinName, balance }: TProps) { + const { t } = useTranslation(); + const nameCol = ( + +
+ + + {coinName} + + + { coinName } + +
+ + ); + if (!balance) { + return null; + } + return ( + + { nameCol } + + + + {' '} + {balance.unit} + + + + + + + ); +}