Skip to content

Commit

Permalink
Merge pull request #2573 from strmci/add_coins_total_balance
Browse files Browse the repository at this point in the history
frontend/account-summary: add coins total balance
  • Loading branch information
strmci authored Mar 26, 2024
2 parents 5954df1 + 8239c92 commit 12ba2a4
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 3 deletions.
54 changes: 54 additions & 0 deletions backend/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,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")
Expand Down Expand Up @@ -747,6 +748,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 {
Expand Down
8 changes: 8 additions & 0 deletions frontends/web/src/api/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ export const getAccountsTotalBalance = (): Promise<TAccountsTotalBalanceResponse
return apiGet('accounts/total-balance');
};

export type TCoinsTotalBalance = {
[key: string]: IAmount;
};

export const getCoinsTotalBalance = (): Promise<TCoinsTotalBalance> => {
return apiGet('accounts/coins-balance');
};

type TEthAccountCodeAndNameByAddress = SuccessResponse & {
code: AccountCode;
name: string;
Expand Down
1 change: 1 addition & 0 deletions frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 24 additions & 3 deletions frontends/web/src/routes/account/summary/accountssummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,6 +61,7 @@ export function AccountsSummary({
const [summaryData, setSummaryData] = useState<accountApi.ISummary>();
const [balancePerCoin, setBalancePerCoin] = useState<accountApi.TAccountsBalance>();
const [accountsTotalBalance, setAccountsTotalBalance] = useState<accountApi.TAccountsTotalBalance>();
const [coinsTotalBalance, setCoinsTotalBalance] = useState<accountApi.TCoinsTotalBalance>();
const [balances, setBalances] = useState<Balances>();

const hasCard = useSDCard(devices);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(() => {
Expand All @@ -177,8 +191,8 @@ export function AccountsSummary({
onStatusChanged(account.code);
});
getAccountsBalance();
}, [onStatusChanged, getAccountsBalance, accounts]);

getCoinsTotalBalance();
}, [onStatusChanged, getAccountsBalance, getCoinsTotalBalance, accounts]);
return (
<GuideWrapper>
<GuidedContent>
Expand All @@ -198,6 +212,13 @@ export function AccountsSummary({
<AddBuyReceiveOnEmptyBalances accounts={accounts} balances={balances} />
) : undefined
} />
{accountsByKeystore.length > 1 && (
<CoinBalance
accounts={accounts}
summaryData={summaryData}
coinsBalances={coinsTotalBalance}
/>
)}
{accountsByKeystore &&
(accountsByKeystore.map(({ keystore, accounts }) =>
<SummaryBalance
Expand Down
113 changes: 113 additions & 0 deletions frontends/web/src/routes/account/summary/coinbalance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Copyright 2024 Shift Crypto AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useTranslation } from 'react-i18next';
import * as accountApi from '../../../api/account';
import { SubTotalCoinRow } from './subtotalrow';
import { Amount } from '../../../components/amount/amount';
import { Skeleton } from '../../../components/skeleton/skeleton';
import style from './accountssummary.module.css';

type TProps = {
accounts: accountApi.IAccount[],
summaryData?: accountApi.ISummary,
coinsBalances?: accountApi.TCoinsTotalBalance,
}

type TAccountCoinMap = {
[code in accountApi.CoinCode]: accountApi.IAccount[];
};

export function CoinBalance ({
accounts,
summaryData,
coinsBalances,
}: TProps) {
const { t } = useTranslation();

const getAccountsPerCoin = () => {
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 (
<div>
<div className={style.accountName}>
<p>{t('accountSummary.total')}</p>
</div>
<div className={style.balanceTable}>
<table className={style.table}>
<colgroup>
<col width="33%" />
<col width="33%" />
<col width="*" />
</colgroup>
<thead>
<tr>
<th>{t('accountSummary.coin')}</th>
<th>{t('accountSummary.balance')}</th>
<th>{t('accountSummary.fiatBalance')}</th>
</tr>
</thead>
<tbody>
{ accounts.length > 0 ? (
coins.map(coinCode => {
if (accountsPerCoin[coinCode]?.length >= 1) {
const account = accountsPerCoin[coinCode][0];
return (
<SubTotalCoinRow
key={account.coinCode}
coinCode={account.coinCode}
coinName={account.coinName}
balance={coinsBalances && coinsBalances[coinCode]}
/>
);
}
return null;
})) : null}
</tbody>
<tfoot>
<tr>
<th>
<strong>{t('accountSummary.total')}</strong>
</th>
<td colSpan={2}>
{(summaryData && summaryData.formattedChartTotal !== null) ? (
<>
<strong>
<Amount amount={summaryData.formattedChartTotal} unit={summaryData.chartFiat}/>
</strong>
{' '}
<span className={style.coinUnit}>
{summaryData.chartFiat}
</span>
</>
) : (<Skeleton />) }
</td>
</tr>
</tfoot>
</table>
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions frontends/web/src/routes/account/summary/subtotalrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,36 @@ export function SubTotalRow ({ coinCode, coinName, balance }: TProps) {
</tr>
);
}


export function SubTotalCoinRow ({ coinCode, coinName, balance }: TProps) {
const { t } = useTranslation();
const nameCol = (
<td data-label={t('accountSummary.total')}>
<div className={style.coinName}>
<Logo className={style.coincode} coinCode={coinCode} active={true} alt={coinCode} />
<span className={style.showOnTableView}>
{coinName}
</span>
</div>
</td>
);
if (!balance) {
return null;
}
return (
<tr key={`${coinCode}_subtotal`} className={style.subTotal}>
{ nameCol }
<td data-label={t('accountSummary.balance')}>
<span className={style.summaryTableBalance}>
<Amount amount={balance.amount} unit={balance.unit}/>
{' '}
<span className={style.coinUnit}>{balance.unit}</span>
</span>
</td>
<td data-label={t('accountSummary.fiatBalance')}>
<FiatConversion amount={balance} noAction={true} />
</td>
</tr>
);
}

0 comments on commit 12ba2a4

Please sign in to comment.