Skip to content

Commit

Permalink
backend/btc: do not panic when retrieving balance
Browse files Browse the repository at this point in the history
Similar to 52da637, where the same as
done for the transactions list.

Fixes one panic TODO. All call sites deal with the error now. An error
here is usually a database access for a database that was already
closed (e.g. transactions endpoint called at the same time as a
bitbox02 is unplugged or the account is closed for some other reason).
  • Loading branch information
benma committed Apr 21, 2023
1 parent 4fbb3b3 commit 14927eb
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 58 deletions.
7 changes: 1 addition & 6 deletions backend/coins/btc/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,12 +544,7 @@ func (account *Account) Balance() (*accounts.Balance, error) {
if account.fatalError.Load() {
return nil, errp.New("can't call Balance() after a fatal error")
}
balance, err := account.transactions.Balance()
if err != nil {
// TODO
panic(err)
}
return balance, nil
return account.transactions.Balance()
}

func (account *Account) incAndEmitSyncCounter() {
Expand Down
22 changes: 15 additions & 7 deletions backend/coins/btc/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,16 +326,24 @@ func (handlers *Handlers) getUTXOs(_ *http.Request) (interface{}, error) {
}

func (handlers *Handlers) getAccountBalance(_ *http.Request) (interface{}, error) {
var result struct {
Success bool `json:"success"`
HasAvailable bool `json:"hasAvailable"`
Available FormattedAmount `json:"available"`
HasIncoming bool `json:"hasIncoming"`
Incoming FormattedAmount `json:"incoming"`
}
balance, err := handlers.account.Balance()
if err != nil {
return nil, err
handlers.log.WithError(err).Error("Error getting account balance")
return result, nil
}
return map[string]interface{}{
"hasAvailable": balance.Available().BigInt().Sign() > 0,
"available": handlers.formatAmountAsJSON(balance.Available(), false),
"hasIncoming": balance.Incoming().BigInt().Sign() > 0,
"incoming": handlers.formatAmountAsJSON(balance.Incoming(), false),
}, nil
result.Success = true
result.HasAvailable = balance.Available().BigInt().Sign() > 0
result.Available = handlers.formatAmountAsJSON(balance.Available(), false)
result.HasIncoming = balance.Incoming().BigInt().Sign() > 0
result.Incoming = handlers.formatAmountAsJSON(balance.Incoming(), false)
return result, nil
}

type sendTxInput struct {
Expand Down
5 changes: 4 additions & 1 deletion frontends/web/src/api/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { apiGet, apiPost } from '../utils/request';
import { SuccessResponse } from './response';
import { ChartData } from '../routes/account/summary/chart';

export type CoinCode = 'btc' | 'tbtc' | 'ltc' | 'tltc' | 'eth' | 'goeth';
Expand Down Expand Up @@ -151,7 +152,9 @@ export interface IBalance {
incoming: IAmount;
}

export const getBalance = (code: AccountCode): Promise<IBalance> => {
export type TBalanceResult = { success: false } | (SuccessResponse & IBalance);

export const getBalance = (code: AccountCode): Promise<TBalanceResult> => {
return apiGet(`account/${code}/balance`);
};

Expand Down
5 changes: 3 additions & 2 deletions frontends/web/src/components/balance/balance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
*/

import { render } from '@testing-library/react';
import { IBalance } from '../../api/account';
import { TBalanceResult } from '../../api/account';
import I18NWrapper from '../../i18n/forTests/i18nwrapper';
import { Balance } from './balance';

describe('components/balance/balance', () => {
it('renders balance properly', () => {
const MOCK_BALANCE: IBalance = {
const MOCK_BALANCE: TBalanceResult = {
success: true,
hasAvailable: true,
hasIncoming: true,
available: {
Expand Down
9 changes: 7 additions & 2 deletions frontends/web/src/components/balance/balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
*/

import { useTranslation } from 'react-i18next';
import { IBalance } from '../../api/account';
import { TBalanceResult } from '../../api/account';
import { FiatConversion } from '../../components/rates/rates';
import { bitcoinRemoveTrailingZeroes } from '../../utils/trailing-zeroes';
import style from './balance.module.css';

type TProps = {
balance?: IBalance;
balance?: TBalanceResult;
noRotateFiat?: boolean;
}

Expand All @@ -36,6 +36,11 @@ export const Balance = ({
<header className={style.balance}></header>
);
}
if (!balance.success) {
return (
<header className={style.balance}>{t('account.balanceError')}</header>
);
}

// remove trailing zeroes from Bitcoin balance
const availableBalance = bitcoinRemoveTrailingZeroes(balance.available.amount, balance.available.unit);
Expand Down
5 changes: 3 additions & 2 deletions frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"initializing": "Getting information from the blockchain…",
"maybeProxyError": "Tor proxy enabled. Ensure that your Tor proxy is running properly, or disable the proxy setting.",
"reconnecting": "Lost connection, trying to reconnect…",
"syncedAddressesCount": "Scanned {{count}} addresses"
"syncedAddressesCount": "Scanned {{count}} addresses",
"balanceError": "Error retrieving balance"
},
"accountInfo": {
"address": "Address",
Expand Down Expand Up @@ -1446,4 +1447,4 @@
"insertDevice": "Please connect your device to get started",
"title": "Welcome"
}
}
}
11 changes: 6 additions & 5 deletions frontends/web/src/routes/account/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function Account({
}: Props) {
const { t } = useTranslation();

const [balance, setBalance] = useState<accountApi.IBalance>();
const [balance, setBalance] = useState<accountApi.TBalanceResult>();
const [status, setStatus] = useState<accountApi.IStatus>();
const [syncedAddressesCount, setSyncedAddressesCount] = useState<number>();
const [transactions, setTransactions] = useState<accountApi.TTransactions>();
Expand Down Expand Up @@ -166,7 +166,7 @@ export function Account({
return null;
}

const canSend = balance && balance.hasAvailable;
const canSend = balance && balance.success && balance.hasAvailable;

const initializingSpinnerText =
(syncedAddressesCount !== undefined && syncedAddressesCount > 1) ? (
Expand All @@ -188,6 +188,7 @@ export function Account({
const exchangeBuySupported = supportedExchanges && supportedExchanges.exchanges.length > 0;

const isAccountEmpty = balance
&& balance.success
&& !balance.hasAvailable
&& !balance.hasIncoming
&& transactions
Expand Down Expand Up @@ -265,10 +266,10 @@ export function Account({
</div>
<AccountGuide
account={account}
unit={balance?.available.unit}
hasIncomingBalance={balance && balance.hasIncoming}
unit={balance?.success ? balance?.available.unit : undefined}
hasIncomingBalance={balance && balance.success && balance.hasIncoming}
hasTransactions={transactions !== undefined && transactions.success && transactions.list.length > 0}
hasNoBalance={balance && balance.available.amount === '0'} />
hasNoBalance={balance && balance.success && balance.available.amount === '0'} />
</div>
);
}
8 changes: 4 additions & 4 deletions frontends/web/src/routes/account/info/buyReceiveCTA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@

import { useTranslation } from 'react-i18next';
import { route } from '../../../utils/route';
import { CoinWithSAT, IBalance } from '../../../api/account';
import { CoinWithSAT, TBalanceResult } from '../../../api/account';
import { Button } from '../../../components/forms';
import { Balances } from '../summary/accountssummary';
import styles from './buyReceiveCTA.module.css';
import { isBitcoinCoin } from '../utils';

type TBuyReceiveCTAProps = {
balanceList?: [string, IBalance][];
balanceList?: [string, TBalanceResult][];
code?: string;
unit?: string;
};
Expand Down Expand Up @@ -59,10 +59,10 @@ export const AddBuyReceiveOnEmptyBalances = ({ balances }: {balances?: Balances}
return null;
}
const balanceList = Object.entries(balances);
if (balanceList.some(entry => entry[1].hasAvailable)) {
if (balanceList.some(entry => entry[1].success && entry[1].hasAvailable)) {
return null;
}
if (balanceList.map(entry => entry[1].available.unit).every(isBitcoinCoin)) {
if (balanceList.every(entry => entry[1].success && isBitcoinCoin(entry[1].available.unit))) {
return <BuyReceiveCTA code={balanceList[0][0]} unit={'BTC'} balanceList={balanceList} />;
}
return <BuyReceiveCTA balanceList={balanceList} />;
Expand Down
4 changes: 2 additions & 2 deletions frontends/web/src/routes/account/send/send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ type Props = SendProps & TranslateProps;

interface State {
account?: accountApi.IAccount;
balance?: accountApi.IBalance;
balance?: accountApi.TBalanceResult;
proposedFee?: accountApi.IAmount;
proposedTotal?: accountApi.IAmount;
recipientAddress: string;
Expand Down Expand Up @@ -680,7 +680,7 @@ class Send extends Component<Props, State> {
type="number"
step="any"
min="0"
label={balance ? balance.available.unit : t('send.amount.label')}
label={balance && balance.success ? balance.available.unit : t('send.amount.label')}
id="amount"
onInput={this.handleFormChange}
disabled={sendAll}
Expand Down
13 changes: 9 additions & 4 deletions frontends/web/src/routes/account/summary/accountssummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface AccountSummaryProps {
}

export interface Balances {
[code: string]: accountApi.IBalance;
[code: string]: accountApi.TBalanceResult;
}

interface SyncStatus {
Expand Down Expand Up @@ -214,12 +214,17 @@ class AccountsSummary extends Component<Props, State> {
{ nameCol }
<td data-label={t('accountSummary.balance')}>
<span className={style.summaryTableBalance}>
{balance.available.amount}{' '}
<span className={style.coinUnit}>{balance.available.unit}</span>
{ balance.success ? (
<>
{balance.available.amount}{' '}
<span className={style.coinUnit}>{balance.available.unit}</span>
</>
) : <>{t('account.balanceError')}</>
}
</span>
</td>
<td data-label={t('accountSummary.fiatBalance')}>
<FiatConversion amount={balance.available} noAction={true} />
{ balance.success && <FiatConversion amount={balance.available} noAction={true} /> }
</td>
</tr>
);
Expand Down
26 changes: 15 additions & 11 deletions frontends/web/src/routes/accounts/select-receive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { getBalance, IAccount } from '../../api/account';
import { AccountSelector, TOption } from '../../components/accountselector/accountselector';
Expand All @@ -31,13 +31,26 @@ export const ReceiveAccountsSelector = ({ activeAccounts }: TReceiveAccountsSele
const [code, setCode] = useState('');
const { t } = useTranslation();

const getBalances = useCallback(async (options: TOption[]) => {
return Promise.all(options.map((option) => (
getBalance(option.value).then(balance => {
return {
...option,
balance: balance.success ?
`${balance.available.amount} ${balance.available.unit}` :
t('account.balanceError'),
};
})
)));
}, [t]);

useEffect(() => {
const options = activeAccounts.map(account => ({ label: account.name, value: account.code, disabled: false, coinCode: account.coinCode } as TOption));
//setting options without balance
setOptions(options);
//asynchronously fetching each account's balance
getBalances(options).then(options => setOptions(options));
}, [activeAccounts]);
}, [activeAccounts, getBalances]);

const handleProceed = () => {
route(`/account/${code}/receive`);
Expand All @@ -47,14 +60,6 @@ export const ReceiveAccountsSelector = ({ activeAccounts }: TReceiveAccountsSele

const title = t('receive.title', { accountName: hasOnlyBTCAccounts ? 'Bitcoin' : t('buy.info.crypto') });

const getBalances = async (options: TOption[]) => {
return Promise.all(options.map((option) => (
getBalance(option.value).then(balance => {
return { ...option, balance: `${balance.available.amount} ${balance.available.unit}` };
})
)));
};

return (
<>
<Header title={<h2>{title}</h2>} />
Expand All @@ -67,4 +72,3 @@ export const ReceiveAccountsSelector = ({ activeAccounts }: TReceiveAccountsSele

);
};

28 changes: 16 additions & 12 deletions frontends/web/src/routes/buy/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ export const BuyInfo = ({ code, accounts }: TProps) => {

const { t } = useTranslation();

const getBalances = useCallback((options: TOption[]) => {
Promise.all(options.map((option) => (
getBalance(option.value).then(balance => {
return {
...option,
balance: balance.success ?
`${balance.available.amount} ${balance.available.unit}` :
t('account.balanceError'),
};
})
))).then(options => {
setOptions(options);
});
}, [t]);

const checkSupportedCoins = useCallback(async () => {
try {
const accountsWithFalsyValue = await Promise.all(
Expand All @@ -56,7 +71,7 @@ export const BuyInfo = ({ code, accounts }: TProps) => {
console.error(e);
}

}, [accounts]);
}, [accounts, getBalances]);

const maybeProceed = useCallback(() => {
if (options !== undefined && options.length === 1) {
Expand All @@ -76,17 +91,6 @@ export const BuyInfo = ({ code, accounts }: TProps) => {
maybeProceed();
}, [maybeProceed, options]);


const getBalances = (options: TOption[]) => {
Promise.all(options.map((option) => (
getBalance(option.value).then(balance => {
return { ...option, balance: `${balance.available.amount} ${balance.available.unit}` };
})
))).then(options => {
setOptions(options);
});
};

const handleProceed = () => {
route(`/buy/exchange/${selected}`);
};
Expand Down

0 comments on commit 14927eb

Please sign in to comment.