Skip to content

Commit ce1bb69

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. Restored backend code previously removed in commit d8c6ba0.
1 parent 61ddaad commit ce1bb69

File tree

6 files changed

+233
-3
lines changed

6 files changed

+233
-3
lines changed

backend/handlers/handlers.go

+54
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

+8
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

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"accountSummary": {
4242
"availableBalance": "Available balance",
4343
"balance": "Balance",
44+
"coin": "Coin",
4445
"exportSummary": "Export accounts summary to downloads folder as CSV file",
4546
"fiatBalance": "Fiat balance",
4647
"name": "Account name",

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

+24-3
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';
@@ -60,6 +61,7 @@ export function AccountsSummary({
6061
const [summaryData, setSummaryData] = useState<accountApi.ISummary>();
6162
const [balancePerCoin, setBalancePerCoin] = useState<accountApi.TAccountsBalance>();
6263
const [accountsTotalBalance, setAccountsTotalBalance] = useState<accountApi.TAccountsTotalBalance>();
64+
const [coinsTotalBalance, setCoinsTotalBalance] = useState<accountApi.TCoinsTotalBalance>();
6365
const [balances, setBalances] = useState<Balances>();
6466

6567
const hasCard = useSDCard(devices);
@@ -109,6 +111,17 @@ export function AccountsSummary({
109111
}
110112
}, [mounted]);
111113

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

113126
const onStatusChanged = useCallback(async (
114127
code: accountApi.AccountCode,
@@ -157,7 +170,8 @@ export function AccountsSummary({
157170
getAccountSummary();
158171
getAccountsBalance();
159172
getAccountsTotalBalance();
160-
}, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, defaultCurrency]);
173+
getCoinsTotalBalance();
174+
}, [getAccountSummary, getAccountsBalance, getAccountsTotalBalance, getCoinsTotalBalance, defaultCurrency]);
161175

162176
// update the timer to get a new account summary update when receiving the previous call result.
163177
useEffect(() => {
@@ -177,8 +191,8 @@ export function AccountsSummary({
177191
onStatusChanged(account.code);
178192
});
179193
getAccountsBalance();
180-
}, [onStatusChanged, getAccountsBalance, accounts]);
181-
194+
getCoinsTotalBalance();
195+
}, [onStatusChanged, getAccountsBalance, getCoinsTotalBalance, accounts]);
182196
return (
183197
<GuideWrapper>
184198
<GuidedContent>
@@ -198,6 +212,13 @@ export function AccountsSummary({
198212
<AddBuyReceiveOnEmptyBalances accounts={accounts} balances={balances} />
199213
) : undefined
200214
} />
215+
{accountsByKeystore.length > 1 && (
216+
<CoinBalance
217+
accounts={accounts}
218+
summaryData={summaryData}
219+
coinsBalances={coinsTotalBalance}
220+
/>
221+
)}
201222
{accountsByKeystore &&
202223
(accountsByKeystore.map(({ keystore, accounts }) =>
203224
<SummaryBalance
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright 2024 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>
55+
<div className={style.accountName}>
56+
<p>{t('accountSummary.total')}</p>
57+
</div>
58+
<div className={style.balanceTable}>
59+
<table className={style.table}>
60+
<colgroup>
61+
<col width="33%" />
62+
<col width="33%" />
63+
<col width="*" />
64+
</colgroup>
65+
<thead>
66+
<tr>
67+
<th>{t('accountSummary.coin')}</th>
68+
<th>{t('accountSummary.balance')}</th>
69+
<th>{t('accountSummary.fiatBalance')}</th>
70+
</tr>
71+
</thead>
72+
<tbody>
73+
{ accounts.length > 0 ? (
74+
coins.map(coinCode => {
75+
if (accountsPerCoin[coinCode]?.length >= 1) {
76+
const account = accountsPerCoin[coinCode][0];
77+
return (
78+
<SubTotalCoinRow
79+
key={account.coinCode}
80+
coinCode={account.coinCode}
81+
coinName={account.coinName}
82+
balance={coinsBalances && coinsBalances[coinCode]}
83+
/>
84+
);
85+
}
86+
return null;
87+
})) : null}
88+
</tbody>
89+
<tfoot>
90+
<tr>
91+
<th>
92+
<strong>{t('accountSummary.total')}</strong>
93+
</th>
94+
<td colSpan={2}>
95+
{(summaryData && summaryData.formattedChartTotal !== null) ? (
96+
<>
97+
<strong>
98+
<Amount amount={summaryData.formattedChartTotal} unit={summaryData.chartFiat}/>
99+
</strong>
100+
{' '}
101+
<span className={style.coinUnit}>
102+
{summaryData.chartFiat}
103+
</span>
104+
</>
105+
) : (<Skeleton />) }
106+
</td>
107+
</tr>
108+
</tfoot>
109+
</table>
110+
</div>
111+
</div>
112+
);
113+
}

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

+33
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,36 @@ 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+
</div>
80+
</td>
81+
);
82+
if (!balance) {
83+
return null;
84+
}
85+
return (
86+
<tr key={`${coinCode}_subtotal`} className={style.subTotal}>
87+
{ nameCol }
88+
<td data-label={t('accountSummary.balance')}>
89+
<span className={style.summaryTableBalance}>
90+
<Amount amount={balance.amount} unit={balance.unit}/>
91+
{' '}
92+
<span className={style.coinUnit}>{balance.unit}</span>
93+
</span>
94+
</td>
95+
<td data-label={t('accountSummary.fiatBalance')}>
96+
<FiatConversion amount={balance} noAction={true} />
97+
</td>
98+
</tr>
99+
);
100+
}

0 commit comments

Comments
 (0)