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}`); };