Skip to content

Commit 1744a10

Browse files
committed
frontend/account-summary: add coins total balance
Before this update, the account summary didn't show total coin balances, especially when managing multiple wallets with the watch-only feature. This commit addresses that by adding a new coin balances table below the chart. It appears only when there are multiple wallets in the portfolio.
1 parent 6400996 commit 1744a10

File tree

6 files changed

+243
-3
lines changed

6 files changed

+243
-3
lines changed

backend/handlers/handlers.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ func NewHandlers(
213213
getAPIRouterNoError(apiRouter)("/keystores", handlers.getKeystores).Methods("GET")
214214
getAPIRouterNoError(apiRouter)("/accounts", handlers.getAccounts).Methods("GET")
215215
getAPIRouter(apiRouter)("/accounts/balance", handlers.getAccountsBalance).Methods("GET")
216+
getAPIRouter(apiRouter)("/accounts/coins-balance", handlers.getCoinsTotalBalance).Methods("GET")
216217
getAPIRouter(apiRouter)("/accounts/total-balance", handlers.getAccountsTotalBalance).Methods("GET")
217218
getAPIRouterNoError(apiRouter)("/set-account-active", handlers.postSetAccountActive).Methods("POST")
218219
getAPIRouterNoError(apiRouter)("/set-token-active", handlers.postSetTokenActive).Methods("POST")
@@ -749,6 +750,59 @@ func (handlers *Handlers) getAccountsBalance(*http.Request) (interface{}, error)
749750
return totalAmount, nil
750751
}
751752

753+
// getCoinsTotalBalance returns the total balances grouped by coins
754+
func (handlers *Handlers) getCoinsTotalBalance(_ *http.Request) (interface{}, error) {
755+
totalPerCoin := make(map[coin.Code]*big.Int)
756+
conversionsPerCoin := make(map[coin.Code]map[string]string)
757+
758+
totalAmount := make(map[coin.Code]accountHandlers.FormattedAmount)
759+
760+
for _, account := range handlers.backend.Accounts() {
761+
if account.Config().Config.Inactive || account.Config().Config.HiddenBecauseUnused {
762+
continue
763+
}
764+
if account.FatalError() {
765+
continue
766+
}
767+
err := account.Initialize()
768+
if err != nil {
769+
return nil, err
770+
}
771+
coinCode := account.Coin().Code()
772+
b, err := account.Balance()
773+
if err != nil {
774+
return nil, err
775+
}
776+
amount := b.Available()
777+
if _, ok := totalPerCoin[coinCode]; !ok {
778+
totalPerCoin[coinCode] = amount.BigInt()
779+
780+
} else {
781+
totalPerCoin[coinCode] = new(big.Int).Add(totalPerCoin[coinCode], amount.BigInt())
782+
}
783+
784+
conversionsPerCoin[coinCode] = coin.Conversions(
785+
coin.NewAmount(totalPerCoin[coinCode]),
786+
account.Coin(),
787+
false,
788+
account.Config().RateUpdater,
789+
util.FormatBtcAsSat(handlers.backend.Config().AppConfig().Backend.BtcUnit))
790+
}
791+
792+
for k, v := range totalPerCoin {
793+
currentCoin, err := handlers.backend.Coin(k)
794+
if err != nil {
795+
return nil, err
796+
}
797+
totalAmount[k] = accountHandlers.FormattedAmount{
798+
Amount: currentCoin.FormatAmount(coin.NewAmount(v), false),
799+
Unit: currentCoin.GetFormatUnit(false),
800+
Conversions: conversionsPerCoin[k],
801+
}
802+
}
803+
return totalAmount, nil
804+
}
805+
752806
// getAccountsTotalBalanceHandler returns the total balance of all the accounts, gruped by keystore.
753807
func (handlers *Handlers) getAccountsTotalBalance(*http.Request) (interface{}, error) {
754808
type response struct {

frontends/web/src/api/account.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ export const getAccountsTotalBalance = (): Promise<TAccountsTotalBalanceResponse
103103
return apiGet('accounts/total-balance');
104104
};
105105

106+
export type TCoinsTotalBalance = {
107+
[key: string]: IAmount;
108+
};
109+
110+
export const getCoinsTotalBalance = (): Promise<TCoinsTotalBalance> => {
111+
return apiGet('accounts/coins-balance');
112+
};
113+
106114
type TEthAccountCodeAndNameByAddress = SuccessResponse & {
107115
code: AccountCode;
108116
name: string;

frontends/web/src/locales/en/app.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"xpubTypeInfo": "Currently displaying {{scriptType}} extended public key ({{current}} of {{numberOfXPubs}})"
4040
},
4141
"accountSummary": {
42+
"coin": "Coin",
4243
"availableBalance": "Available balance",
4344
"balance": "Balance",
4445
"exportSummary": "Export accounts summary to downloads folder as CSV file",

frontends/web/src/routes/account/summary/accountssummary.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { GuideWrapper, GuidedContent, Header, Main } from '../../../components/l
2828
import { View } from '../../../components/view/view';
2929
import { Chart } from './chart';
3030
import { SummaryBalance } from './summarybalance';
31+
import { CoinBalance } from './coinbalance';
3132
import { AddBuyReceiveOnEmptyBalances } from '../info/buyReceiveCTA';
3233
import { Entry } from '../../../components/guide/entry';
3334
import { Guide } from '../../../components/guide/guide';
@@ -58,6 +59,7 @@ export function AccountsSummary({
5859
const [summaryData, setSummaryData] = useState<accountApi.ISummary>();
5960
const [balancePerCoin, setBalancePerCoin] = useState<accountApi.TAccountsBalance>();
6061
const [accountsTotalBalance, setAccountsTotalBalance] = useState<accountApi.TAccountsTotalBalance>();
62+
const [coinsTotalBalance, setCoinsTotalBalance] = useState<accountApi.TCoinsTotalBalance>();
6163
const [balances, setBalances] = useState<Balances>();
6264

6365
const hasCard = useSDCard(devices);
@@ -107,6 +109,17 @@ export function AccountsSummary({
107109
}
108110
}, [mounted]);
109111

112+
const getCoinsTotalBalance = useCallback(async () => {
113+
try {
114+
const coinBalance = await accountApi.getCoinsTotalBalance();
115+
if (!mounted.current) {
116+
return;
117+
}
118+
setCoinsTotalBalance(coinBalance);
119+
} catch (err) {
120+
console.error(err);
121+
}
122+
}, [mounted]);
110123

111124
const onStatusChanged = useCallback(async (
112125
code: accountApi.AccountCode,
@@ -147,8 +160,9 @@ export function AccountsSummary({
147160
getAccountSummary();
148161
getAccountsBalance();
149162
getAccountsTotalBalance();
163+
getCoinsTotalBalance();
150164
return () => unsubscribe(subscriptions);
151-
}, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, update]);
165+
}, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, getCoinsTotalBalance, update]);
152166

153167
// update the timer to get a new account summary update when receiving the previous call result.
154168
useEffect(() => {
@@ -168,8 +182,8 @@ export function AccountsSummary({
168182
onStatusChanged(account.code);
169183
});
170184
getAccountsBalance();
171-
}, [onStatusChanged, getAccountsBalance, accounts]);
172-
185+
getCoinsTotalBalance();
186+
}, [onStatusChanged, getAccountsBalance, getCoinsTotalBalance, accounts]);
173187
return (
174188
<GuideWrapper>
175189
<GuidedContent>
@@ -189,6 +203,13 @@ export function AccountsSummary({
189203
<AddBuyReceiveOnEmptyBalances accounts={accounts} balances={balances} />
190204
) : undefined
191205
} />
206+
{accountsByKeystore.length > 1 && (
207+
<CoinBalance
208+
accounts={accounts}
209+
summaryData={summaryData}
210+
coinsBalances={coinsTotalBalance}
211+
/>
212+
)}
192213
{accountsByKeystore &&
193214
(accountsByKeystore.map(({ keystore, accounts }) =>
194215
<SummaryBalance
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Copyright 2023 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useTranslation } from 'react-i18next';
18+
import * as accountApi from '../../../api/account';
19+
import { SubTotalCoinRow } from './subtotalrow';
20+
import { Amount } from '../../../components/amount/amount';
21+
import { Skeleton } from '../../../components/skeleton/skeleton';
22+
import style from './accountssummary.module.css';
23+
24+
type TProps = {
25+
accounts: accountApi.IAccount[],
26+
summaryData?: accountApi.ISummary,
27+
coinsBalances?: accountApi.TCoinsTotalBalance,
28+
}
29+
30+
type TAccountCoinMap = {
31+
[code in accountApi.CoinCode]: accountApi.IAccount[];
32+
};
33+
34+
export function CoinBalance ({
35+
accounts,
36+
summaryData,
37+
coinsBalances,
38+
}: TProps) {
39+
const { t } = useTranslation();
40+
41+
const getAccountsPerCoin = () => {
42+
return accounts.reduce((accountPerCoin, account) => {
43+
accountPerCoin[account.coinCode]
44+
? accountPerCoin[account.coinCode].push(account)
45+
: accountPerCoin[account.coinCode] = [account];
46+
return accountPerCoin;
47+
}, {} as TAccountCoinMap);
48+
};
49+
50+
const accountsPerCoin = getAccountsPerCoin();
51+
const coins = Object.keys(accountsPerCoin) as accountApi.CoinCode[];
52+
53+
return (
54+
<div className={style.balanceTable}>
55+
<table className={style.table}>
56+
<colgroup>
57+
<col width="33%" />
58+
<col width="33%" />
59+
<col width="*" />
60+
</colgroup>
61+
<thead>
62+
<tr>
63+
<th>{t('accountSummary.coin')}</th>
64+
<th>{t('accountSummary.balance')}</th>
65+
<th>{t('accountSummary.fiatBalance')}</th>
66+
</tr>
67+
</thead>
68+
<tbody>
69+
{ accounts.length > 0 ? (
70+
coins.map(coinCode => {
71+
if (accountsPerCoin[coinCode]?.length > 1) {
72+
const account = accountsPerCoin[coinCode][0];
73+
return (
74+
<SubTotalCoinRow
75+
key={account.coinCode}
76+
coinCode={account.coinCode}
77+
coinName={account.coinName}
78+
balance={coinsBalances && coinsBalances[coinCode]}
79+
/>
80+
);
81+
}
82+
return (
83+
<tr>
84+
<td colSpan={3} className={style.noAccount}>
85+
{t('accountSummary.noAccount')}
86+
</td>
87+
</tr>
88+
);
89+
})) : (
90+
<tr>
91+
<td colSpan={3} className={style.noAccount}>
92+
{t('accountSummary.noAccount')}
93+
</td>
94+
</tr>
95+
)}
96+
</tbody>
97+
<tfoot>
98+
<tr>
99+
<th>
100+
<strong>{t('accountSummary.total')}</strong>
101+
</th>
102+
<td colSpan={2}>
103+
{(summaryData && summaryData.formattedChartTotal !== null) ? (
104+
<>
105+
<strong>
106+
<Amount amount={summaryData.formattedChartTotal} unit={summaryData.chartFiat}/>
107+
</strong>
108+
{' '}
109+
<span className={style.coinUnit}>
110+
{summaryData.chartFiat}
111+
</span>
112+
</>
113+
) : (<Skeleton />) }
114+
</td>
115+
</tr>
116+
</tfoot>
117+
</table>
118+
</div>
119+
);
120+
}

frontends/web/src/routes/account/summary/subtotalrow.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,39 @@ export function SubTotalRow ({ coinCode, coinName, balance }: TProps) {
6565
</tr>
6666
);
6767
}
68+
69+
70+
export function SubTotalCoinRow ({ coinCode, coinName, balance }: TProps) {
71+
const { t } = useTranslation();
72+
const nameCol = (
73+
<td data-label={t('accountSummary.total')}>
74+
<div className={style.coinName}>
75+
<Logo className={style.coincode} coinCode={coinCode} active={true} alt={coinCode} />
76+
<span className={style.showOnTableView}>
77+
{coinName}
78+
</span>
79+
<strong className={style.showInCollapsedView}>
80+
{ coinName }
81+
</strong>
82+
</div>
83+
</td>
84+
);
85+
if (!balance) {
86+
return null;
87+
}
88+
return (
89+
<tr key={`${coinCode}_subtotal`} className={style.subTotal}>
90+
{ nameCol }
91+
<td data-label={t('accountSummary.balance')}>
92+
<span className={style.summaryTableBalance}>
93+
<Amount amount={balance.amount} unit={balance.unit}/>
94+
{' '}
95+
<span className={style.coinUnit}>{balance.unit}</span>
96+
</span>
97+
</td>
98+
<td data-label={t('accountSummary.fiatBalance')}>
99+
<FiatConversion amount={balance} noAction={true} />
100+
</td>
101+
</tr>
102+
);
103+
}

0 commit comments

Comments
 (0)