From 4427b1c2b4c797a817b339f33673b9d0f7dd9224 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Fri, 21 Apr 2023 04:39:43 +0200 Subject: [PATCH] backend/btc: do not panic when retrieving balance Similar to 52da637577bc39d9b4871c03e0c2c28d58a4ff68, 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). --- backend/coins/btc/account.go | 7 +---- backend/coins/btc/handlers/handlers.go | 22 ++++++++++----- frontends/web/src/api/account.ts | 5 +++- .../src/components/balance/balance.test.tsx | 5 ++-- .../web/src/components/balance/balance.tsx | 9 ++++-- frontends/web/src/locales/en/app.json | 1 + frontends/web/src/routes/account/account.tsx | 11 ++++---- .../src/routes/account/info/buyReceiveCTA.tsx | 8 +++--- .../web/src/routes/account/send/send.tsx | 4 +-- .../account/summary/accountssummary.tsx | 13 ++++++--- .../src/routes/accounts/select-receive.tsx | 26 +++++++++-------- frontends/web/src/routes/buy/info.tsx | 28 +++++++++++-------- 12 files changed, 83 insertions(+), 56 deletions(-) diff --git a/backend/coins/btc/account.go b/backend/coins/btc/account.go index c675670a31..179e5c930b 100644 --- a/backend/coins/btc/account.go +++ b/backend/coins/btc/account.go @@ -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() { diff --git a/backend/coins/btc/handlers/handlers.go b/backend/coins/btc/handlers/handlers.go index 6672606c01..1d3cd165ec 100644 --- a/backend/coins/btc/handlers/handlers.go +++ b/backend/coins/btc/handlers/handlers.go @@ -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 { diff --git a/frontends/web/src/api/account.ts b/frontends/web/src/api/account.ts index 3897e41232..136f15cd47 100644 --- a/frontends/web/src/api/account.ts +++ b/frontends/web/src/api/account.ts @@ -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'; @@ -151,7 +152,9 @@ export interface IBalance { incoming: IAmount; } -export const getBalance = (code: AccountCode): Promise => { +export type TBalanceResult = { success: false } | (SuccessResponse & IBalance); + +export const getBalance = (code: AccountCode): Promise => { return apiGet(`account/${code}/balance`); }; diff --git a/frontends/web/src/components/balance/balance.test.tsx b/frontends/web/src/components/balance/balance.test.tsx index f84726be0f..c5f814633e 100644 --- a/frontends/web/src/components/balance/balance.test.tsx +++ b/frontends/web/src/components/balance/balance.test.tsx @@ -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: { diff --git a/frontends/web/src/components/balance/balance.tsx b/frontends/web/src/components/balance/balance.tsx index ff9c5427c3..1500843263 100644 --- a/frontends/web/src/components/balance/balance.tsx +++ b/frontends/web/src/components/balance/balance.tsx @@ -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; } @@ -36,6 +36,11 @@ export const Balance = ({
); } + if (!balance.success) { + return ( +
{t('account.balanceError')}
+ ); + } // remove trailing zeroes from Bitcoin balance const availableBalance = bitcoinRemoveTrailingZeroes(balance.available.amount, balance.available.unit); diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index ed0bdf60c3..ebda374fe4 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -1,5 +1,6 @@ { "account": { + "balanceError": "Error retrieving balance", "disconnect": "Connection lost. Retrying…", "export": "Export", "exportTransactions": "Export transactions to downloads folder as CSV file", diff --git a/frontends/web/src/routes/account/account.tsx b/frontends/web/src/routes/account/account.tsx index 9ca00a18db..35dfd8b258 100644 --- a/frontends/web/src/routes/account/account.tsx +++ b/frontends/web/src/routes/account/account.tsx @@ -54,7 +54,7 @@ export function Account({ }: Props) { const { t } = useTranslation(); - const [balance, setBalance] = useState(); + const [balance, setBalance] = useState(); const [status, setStatus] = useState(); const [syncedAddressesCount, setSyncedAddressesCount] = useState(); const [transactions, setTransactions] = useState(); @@ -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) ? ( @@ -188,6 +188,7 @@ export function Account({ const exchangeBuySupported = supportedExchanges && supportedExchanges.exchanges.length > 0; const isAccountEmpty = balance + && balance.success && !balance.hasAvailable && !balance.hasIncoming && transactions @@ -265,10 +266,10 @@ export function Account({ 0} - hasNoBalance={balance && balance.available.amount === '0'} /> + hasNoBalance={balance && balance.success && balance.available.amount === '0'} /> ); } diff --git a/frontends/web/src/routes/account/info/buyReceiveCTA.tsx b/frontends/web/src/routes/account/info/buyReceiveCTA.tsx index b19d8ab05f..f7d4908d43 100644 --- a/frontends/web/src/routes/account/info/buyReceiveCTA.tsx +++ b/frontends/web/src/routes/account/info/buyReceiveCTA.tsx @@ -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; }; @@ -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 ; } return ; diff --git a/frontends/web/src/routes/account/send/send.tsx b/frontends/web/src/routes/account/send/send.tsx index c32fae2e73..93c6a90f47 100644 --- a/frontends/web/src/routes/account/send/send.tsx +++ b/frontends/web/src/routes/account/send/send.tsx @@ -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; @@ -680,7 +680,7 @@ class Send extends Component { 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} diff --git a/frontends/web/src/routes/account/summary/accountssummary.tsx b/frontends/web/src/routes/account/summary/accountssummary.tsx index 7e30e48531..13b30e321c 100644 --- a/frontends/web/src/routes/account/summary/accountssummary.tsx +++ b/frontends/web/src/routes/account/summary/accountssummary.tsx @@ -40,7 +40,7 @@ interface AccountSummaryProps { } export interface Balances { - [code: string]: accountApi.IBalance; + [code: string]: accountApi.TBalanceResult; } interface SyncStatus { @@ -214,12 +214,17 @@ class AccountsSummary extends Component { { nameCol } - {balance.available.amount}{' '} - {balance.available.unit} + { balance.success ? ( + <> + {balance.available.amount}{' '} + {balance.available.unit} + + ) : <>{t('account.balanceError')} + } - + { balance.success && } ); diff --git a/frontends/web/src/routes/accounts/select-receive.tsx b/frontends/web/src/routes/accounts/select-receive.tsx index e138821fc9..0f13650a42 100644 --- a/frontends/web/src/routes/accounts/select-receive.tsx +++ b/frontends/web/src/routes/accounts/select-receive.tsx @@ -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'; @@ -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`); @@ -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 ( <>
{title}} /> @@ -67,4 +72,3 @@ export const ReceiveAccountsSelector = ({ activeAccounts }: TReceiveAccountsSele ); }; - diff --git a/frontends/web/src/routes/buy/info.tsx b/frontends/web/src/routes/buy/info.tsx index 72decdefbd..3e21288a44 100644 --- a/frontends/web/src/routes/buy/info.tsx +++ b/frontends/web/src/routes/buy/info.tsx @@ -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( @@ -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) { @@ -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}`); };