From d1792356d1460b6f2b106bd6bab6b18ff4ee6d99 Mon Sep 17 00:00:00 2001 From: Tomas Vrba Date: Wed, 27 May 2026 12:05:45 +0200 Subject: [PATCH] eth: speed up active account transaction discovery Incoming ETH transactions are currently only discovered during the regular Etherscan history sync, which runs every five minutes to stay conservative with API usage. That means a mined incoming transaction can remain invisible for minutes even though Ethereum blocks are produced much faster. Add a short-lived foreground activity lease so the backend can identify ETH accounts the user is actively viewing. While such a lease is active, cheaply probe the remote balance and trigger a targeted full account update only when the balance changes. This improves mined incoming transaction discovery without polling full transaction history for every account more often, and the existing five-minute full sync remains the fallback. --- backend/backend.go | 22 ++ backend/coins/eth/account.go | 5 +- backend/coins/eth/updater.go | 247 ++++++++++++++++-- backend/coins/eth/updater_internal_test.go | 27 ++ backend/coins/eth/updater_test.go | 59 +++++ backend/handlers/handlers.go | 22 ++ frontends/web/src/api/account.ts | 7 + frontends/web/src/hooks/account-activity.ts | 41 +++ frontends/web/src/routes/account/account.tsx | 6 +- .../src/routes/account/receive/receive.tsx | 3 + 10 files changed, 413 insertions(+), 26 deletions(-) create mode 100644 backend/coins/eth/updater_internal_test.go create mode 100644 frontends/web/src/hooks/account-activity.ts diff --git a/backend/backend.go b/backend/backend.go index b04554519f..0c580eeee1 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -639,6 +639,28 @@ func (backend *Backend) ManualReconnect(reconnectETH bool) { } } +// SetAccountActivity marks an account as foreground-active for short-lived background refreshes. +func (backend *Backend) SetAccountActivity(accountCode accountsTypes.Code, active bool) error { + for _, account := range backend.Accounts() { + if account.Config().Config.Code != accountCode { + continue + } + ethAccount, ok := account.(*eth.Account) + if !ok { + return nil + } + if active && backend.environment != nil && backend.environment.UsingMobileData() { + active = false + } + backend.ethupdater.SetAccountActivity(ethAccount, active) + return nil + } + if !active { + return nil + } + return errp.Newf("unknown account code %q", accountCode) +} + // Testing returns whether this backend is for testing only. func (backend *Backend) Testing() bool { return backend.testing diff --git a/backend/coins/eth/account.go b/backend/coins/eth/account.go index c32dfb6669..383fa52524 100644 --- a/backend/coins/eth/account.go +++ b/backend/coins/eth/account.go @@ -80,6 +80,7 @@ type Account struct { // updateLock covers balance, blockNumber, nextNonce, transactions and activeTxProposal. updateLock locker.Locker balance coin.Amount + rawBalance coin.Amount blockNumber *big.Int transactions []*accounts.TransactionData @@ -111,6 +112,7 @@ func NewAccount( signingConfiguration: nil, httpClient: httpClient, balance: coin.NewAmountFromInt64(0), + rawBalance: coin.NewAmountFromInt64(0), enqueueUpdateCh: enqueueUpdateCh, @@ -411,8 +413,9 @@ func (account *Account) Update( } } + account.rawBalance = coin.NewAmount(balance) pendingAmount := pendingTxsAmount(outgoingTransactionsData, account.coin.erc20Token != nil) - account.balance = coin.NewAmount(balance.Sub(balance, pendingAmount)) + account.balance = coin.NewAmount(new(big.Int).Sub(balance, pendingAmount)) if account.initDone != nil { account.initDone() diff --git a/backend/coins/eth/updater.go b/backend/coins/eth/updater.go index 2a84396a6f..5126c8a647 100644 --- a/backend/coins/eth/updater.go +++ b/backend/coins/eth/updater.go @@ -12,6 +12,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/etherscan" "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" + "github.com/BitBoxSwiss/bitbox-wallet-app/util/locker" "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" "github.com/ethereum/go-ethereum/common" "github.com/sirupsen/logrus" @@ -21,6 +22,11 @@ import ( // pollInterval is the interval at which the account is polled for updates. var pollInterval = 5 * time.Minute +var activeProbeInterval = time.Minute +var activeProbeLeaseDuration = 90 * time.Second +var activeProbeRetryDelay = 30 * time.Second +var activeProbeBackoffDuration = 5 * time.Minute + // BalanceAndBlockNumberFetcher is an interface that defines a method to fetch balances for a list of addresses, // as well as the block number for a chain. // @@ -50,7 +56,7 @@ type Updater struct { quit chan struct{} // enqueueUpdateForAccount is used to enqueue an update for a specific ETH account. - enqueueUpdateForAccount <-chan *Account + enqueueUpdateForAccount chan *Account // updateETHAccountsCh is used to trigger an update of all ETH accounts. updateETHAccountsCh chan struct{} @@ -62,6 +68,11 @@ type Updater struct { // updateAccounts is a function that updates all ETH accounts. updateAccounts func() error + + activeAccountsLock locker.Locker + activeAccountLeases map[*Account]time.Time + activeProbeBackoffUntil time.Time + timeNow func() time.Time } // NewUpdater creates a new Updater instance. @@ -79,6 +90,8 @@ func NewUpdater( etherscanRateLimiter: etherscanRateLimiter, updateAccounts: updateETHAccounts, log: logging.Get().WithGroup("ethupdater"), + activeAccountLeases: map[*Account]time.Time{}, + timeNow: time.Now, } } @@ -92,6 +105,77 @@ func (u *Updater) EnqueueUpdateForAllAccounts() { u.updateETHAccountsCh <- struct{}{} } +// SetAccountActivity refreshes or clears the foreground activity lease for an ETH account. +func (u *Updater) SetAccountActivity(account *Account, active bool) { + if account == nil { + return + } + defer u.activeAccountsLock.Lock()() + if !active || account.isClosed() { + delete(u.activeAccountLeases, account) + return + } + u.activeAccountLeases[account] = u.timeNow().Add(activeProbeLeaseDuration) +} + +func (u *Updater) activeAccounts() []*Account { + defer u.activeAccountsLock.Lock()() + now := u.timeNow() + accounts := []*Account{} + for account, expiresAt := range u.activeAccountLeases { + if !expiresAt.After(now) || account.isClosed() { + delete(u.activeAccountLeases, account) + continue + } + accounts = append(accounts, account) + } + return accounts +} + +func (u *Updater) activeProbeBackoffActive() bool { + defer u.activeAccountsLock.RLock()() + return u.timeNow().Before(u.activeProbeBackoffUntil) +} + +func (u *Updater) setActiveProbeBackoff() { + defer u.activeAccountsLock.Lock()() + u.activeProbeBackoffUntil = u.timeNow().Add(activeProbeBackoffDuration) +} + +func (u *Updater) scheduleAccountUpdate(account *Account, delay time.Duration) { + if u.enqueueUpdateForAccount == nil { + return + } + go func() { + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-u.quit: + return + case <-timer.C: + } + select { + case <-u.quit: + case u.enqueueUpdateForAccount <- account: + } + }() +} + +func (u *Updater) probeActiveAccounts() { + if u.activeProbeBackoffActive() { + return + } + accountsByChainID := map[string][]*Account{} + for _, account := range u.activeAccounts() { + chainID := account.ETHCoin().ChainIDstr() + accountsByChainID[chainID] = append(accountsByChainID[chainID], account) + } + for chainID, ethAccounts := range accountsByChainID { + etherScanClient := etherscan.NewEtherScan(chainID, u.etherscanClient, u.etherscanRateLimiter) + u.ProbeActiveAccountBalances(ethAccounts, etherScanClient) + } +} + // PollBalances updates the balances of all ETH accounts. // It does that in three different cases: // - When a timer triggers the update. @@ -99,6 +183,7 @@ func (u *Updater) EnqueueUpdateForAllAccounts() { // - When a specific account is updated through EnqueueUpdateForAccount. func (u *Updater) PollBalances() { timer := time.After(0) + activeProbeTimer := time.After(activeProbeInterval) updateAll := func() { if err := u.updateAccounts(); err != nil { @@ -126,14 +211,117 @@ func (u *Updater) PollBalances() { case <-timer: go updateAll() timer = time.After(pollInterval) + case <-activeProbeTimer: + go u.probeActiveAccounts() + activeProbeTimer = time.After(activeProbeInterval) + } + } + } + +} + +func (account *Account) remoteBalance() *big.Int { + defer account.updateLock.RLock()() + return account.rawBalance.BigInt() +} + +// ProbeActiveAccountBalances cheaply checks active account balances and triggers a full update when +// a remote balance changed. +func (u *Updater) ProbeActiveAccountBalances( + ethAccounts []*Account, + etherScanClient BalanceAndBlockNumberFetcher, +) { + if len(ethAccounts) == 0 || u.activeProbeBackoffActive() { + return + } + + activeAccountsSet := map[*Account]struct{}{} + for _, account := range u.activeAccounts() { + activeAccountsSet[account] = struct{}{} + } + + nativeAccountsByAddress := map[common.Address][]*Account{} + nativeAddresses := []common.Address{} + changedAccounts := []*Account{} + changedAccountBalances := map[*Account]*big.Int{} + addChangedAccount := func(account *Account, remoteBalance *big.Int) { + if account.remoteBalance().Cmp(remoteBalance) == 0 { + return + } + if _, ok := changedAccountBalances[account]; ok { + return + } + changedAccountBalances[account] = new(big.Int).Set(remoteBalance) + changedAccounts = append(changedAccounts, account) + } + + for _, account := range ethAccounts { + if _, ok := activeAccountsSet[account]; !ok { + continue + } + if account.isClosed() { + u.SetAccountActivity(account, false) + continue + } + address, err := account.Address() + if err != nil { + u.log.WithError(err).Errorf("Could not get address for account %s", account.Config().Config.Code) + continue + } + if IsERC20(account) { + balance, err := account.coin.client.ERC20Balance(account.address.Address, account.coin.erc20Token) + if err != nil { + u.log.WithError(err).Errorf("Could not probe ERC20 balance for address %s", address.Address.Hex()) + u.setActiveProbeBackoff() + continue } + addChangedAccount(account, balance) + continue + } + if _, ok := nativeAccountsByAddress[address.Address]; !ok { + nativeAddresses = append(nativeAddresses, address.Address) } + nativeAccountsByAddress[address.Address] = append(nativeAccountsByAddress[address.Address], account) } + if len(nativeAddresses) > 0 { + balances, err := etherScanClient.Balances(context.TODO(), nativeAddresses) + if err != nil { + u.log.WithError(err).Error("Could not probe ETH account balances") + u.setActiveProbeBackoff() + } else { + for address, accounts := range nativeAccountsByAddress { + balance, ok := balances[address] + if !ok { + u.log.Errorf("Could not find probed balance for address %s", address.Hex()) + continue + } + for _, account := range accounts { + addChangedAccount(account, balance) + } + } + } + } + + if len(changedAccounts) == 0 { + return + } + u.updateBalancesAndBlockNumber(changedAccounts, etherScanClient, changedAccountBalances) + for _, account := range changedAccounts { + u.scheduleAccountUpdate(account, activeProbeRetryDelay) + } } // UpdateBalancesAndBlockNumber updates the balances of the accounts in the provided slice. func (u *Updater) UpdateBalancesAndBlockNumber(ethAccounts []*Account, etherScanClient BalanceAndBlockNumberFetcher) { + u.updateBalancesAndBlockNumber(ethAccounts, etherScanClient, nil) +} + +func (u *Updater) updateBalancesAndBlockNumber( + ethAccounts []*Account, + etherScanClient BalanceAndBlockNumberFetcher, + prefetchedBalances map[*Account]*big.Int, +) { if len(ethAccounts) == 0 { return } @@ -157,15 +345,22 @@ func (u *Updater) UpdateBalancesAndBlockNumber(ethAccounts []*Account, etherScan continue } if !IsERC20(account) { + if _, ok := prefetchedBalances[account]; ok { + continue + } ethNonErc20Addresses = append(ethNonErc20Addresses, address.Address) } } updateNonERC20 := true - balances, err := etherScanClient.Balances(context.TODO(), ethNonErc20Addresses) - if err != nil { - u.log.WithError(err).Error("Could not get balances for ETH accounts") - updateNonERC20 = false + balances := map[common.Address]*big.Int{} + if len(ethNonErc20Addresses) > 0 || prefetchedBalances == nil { + var err error + balances, err = etherScanClient.Balances(context.TODO(), ethNonErc20Addresses) + if err != nil { + u.log.WithError(err).Error("Could not get balances for ETH accounts") + updateNonERC20 = false + } } blockNumber, err := etherScanClient.BlockNumber(context.TODO()) @@ -189,28 +384,32 @@ func (u *Updater) UpdateBalancesAndBlockNumber(ethAccounts []*Account, etherScan account.SetOffline(err) } var balance *big.Int - switch { - case IsERC20(account): - var err error - balance, err = account.coin.client.ERC20Balance(account.address.Address, account.coin.erc20Token) - if err != nil { - u.log.WithError(err).Errorf("Could not get ERC20 balance for address %s", address.Address.Hex()) - account.SetOffline(err) - } - case updateNonERC20: - var ok bool - balance, ok = balances[address.Address] - if !ok { - errMsg := fmt.Sprintf("Could not find balance for address %s", address.Address.Hex()) + if prefetchedBalance, ok := prefetchedBalances[account]; ok { + balance = new(big.Int).Set(prefetchedBalance) + } else { + switch { + case IsERC20(account): + var err error + balance, err = account.coin.client.ERC20Balance(account.address.Address, account.coin.erc20Token) + if err != nil { + u.log.WithError(err).Errorf("Could not get ERC20 balance for address %s", address.Address.Hex()) + account.SetOffline(err) + } + case updateNonERC20: + var ok bool + balance, ok = balances[address.Address] + if !ok { + errMsg := fmt.Sprintf("Could not find balance for address %s", address.Address.Hex()) + u.log.Error(errMsg) + account.SetOffline(errp.Newf(errMsg)) + } + default: + // If we get there, this is a non-erc20 account and we failed getting balances. + // If we couldn't get the balances for non-erc20 accounts, we mark them as offline + errMsg := fmt.Sprintf("Could not get balance for address %s", address.Address.Hex()) u.log.Error(errMsg) account.SetOffline(errp.Newf(errMsg)) } - default: - // If we get there, this is a non-erc20 account and we failed getting balances. - // If we couldn't get the balances for non-erc20 accounts, we mark them as offline - errMsg := fmt.Sprintf("Could not get balance for address %s", address.Address.Hex()) - u.log.Error(errMsg) - account.SetOffline(errp.Newf(errMsg)) } if account.Offline() != nil { diff --git a/backend/coins/eth/updater_internal_test.go b/backend/coins/eth/updater_internal_test.go new file mode 100644 index 0000000000..77cc6b8cd2 --- /dev/null +++ b/backend/coins/eth/updater_internal_test.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +package eth + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestActiveAccountLease(t *testing.T) { + now := time.Unix(1000, 0) + updater := NewUpdater(nil, nil, nil, nil) + updater.timeNow = func() time.Time { return now } + + account := &Account{} + updater.SetAccountActivity(account, true) + require.Equal(t, []*Account{account}, updater.activeAccounts()) + + now = now.Add(activeProbeLeaseDuration + time.Second) + require.Empty(t, updater.activeAccounts()) + + updater.SetAccountActivity(account, true) + updater.SetAccountActivity(account, false) + require.Empty(t, updater.activeAccounts()) +} diff --git a/backend/coins/eth/updater_test.go b/backend/coins/eth/updater_test.go index 75ffcb1230..3f7b9b500d 100644 --- a/backend/coins/eth/updater_test.go +++ b/backend/coins/eth/updater_test.go @@ -244,6 +244,65 @@ func TestUpdateBalancesWithError(t *testing.T) { } +func TestProbeActiveBalancesOnlyActiveAccounts(t *testing.T) { + activeAccount := newAccount(t, nil, false) + defer activeAccount.Close() + inactiveAccount := newAccount(t, nil, false) + defer inactiveAccount.Close() + + activeAddress, err := activeAccount.Address() + require.NoError(t, err) + + fetcher := &mocks.BalanceAndBlockNumberFetcherMock{ + BalancesFunc: func(ctx context.Context, addresses []common.Address) (map[common.Address]*big.Int, error) { + require.Equal(t, []common.Address{activeAddress.Address}, addresses) + return map[common.Address]*big.Int{ + activeAddress.Address: big.NewInt(0), + }, nil + }, + BlockNumberFunc: func(ctx context.Context) (*big.Int, error) { + require.Fail(t, "unchanged probe should not trigger a full update") + return nil, nil + }, + } + + updater := eth.NewUpdater(nil, nil, nil, nil) + updater.SetAccountActivity(activeAccount, true) + updater.ProbeActiveAccountBalances([]*eth.Account{activeAccount, inactiveAccount}, fetcher) + + require.Len(t, fetcher.BalancesCalls(), 1) + require.Empty(t, fetcher.BlockNumberCalls()) +} + +func TestProbeActiveBalancesTriggersUpdateOnBalanceChange(t *testing.T) { + account := newAccount(t, nil, false) + defer account.Close() + + address, err := account.Address() + require.NoError(t, err) + + remoteBalance := big.NewInt(1000) + fetcher := &mocks.BalanceAndBlockNumberFetcherMock{ + BalancesFunc: func(ctx context.Context, addresses []common.Address) (map[common.Address]*big.Int, error) { + require.Equal(t, []common.Address{address.Address}, addresses) + return map[common.Address]*big.Int{ + address.Address: new(big.Int).Set(remoteBalance), + }, nil + }, + BlockNumberFunc: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(101), nil + }, + } + + updater := eth.NewUpdater(nil, nil, nil, nil) + updater.SetAccountActivity(account, true) + updater.ProbeActiveAccountBalances([]*eth.Account{account}, fetcher) + + require.Len(t, fetcher.BalancesCalls(), 1) + require.Len(t, fetcher.BlockNumberCalls(), 1) + assertAccountBalance(t, account, remoteBalance) +} + func makeConfirmedTx(id string) *accounts.TransactionData { amount := coin.NewAmountFromInt64(1) return &accounts.TransactionData{ diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 04d48c5fef..7b8948c543 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -103,6 +103,7 @@ type Backend interface { CanAddAccount(coinpkg.Code, keystore.Keystore) (string, bool) CreateAndPersistAccountConfig(coinCode coinpkg.Code, name string, keystore keystore.Keystore) (accountsTypes.Code, error) SetAccountActive(accountCode accountsTypes.Code, active bool) error + SetAccountActivity(accountCode accountsTypes.Code, active bool) error SetTokenActive(accountCode accountsTypes.Code, tokenCode string, active bool) error SetAccountReceiveScriptType(accountCode accountsTypes.Code, scriptType signing.ScriptType) error RenameAccount(accountCode accountsTypes.Code, name string) error @@ -224,6 +225,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/swap/accounts", handlers.getSwapAccounts).Methods("GET") getAPIRouterNoError(apiRouter)("/swap/status", handlers.getSwapStatus).Methods("GET") getAPIRouterNoError(apiRouter)("/accounts/balance-summary", handlers.getAccountsBalanceSummary).Methods("GET") + getAPIRouterNoError(apiRouter)("/account/{accountCode}/activity", handlers.postAccountActivity).Methods("POST") getAPIRouterNoError(apiRouter)("/set-account-active", handlers.postSetAccountActive).Methods("POST") getAPIRouterNoError(apiRouter)("/set-token-active", handlers.postSetTokenActive).Methods("POST") getAPIRouterNoError(apiRouter)("/set-account-receive-script-type", handlers.postSetAccountReceiveScriptType).Methods("POST") @@ -909,6 +911,26 @@ func (handlers *Handlers) getAccountsBalanceSummary(*http.Request) interface{} { return response{Success: true, TotalBalance: totalBalance} } +func (handlers *Handlers) postAccountActivity(r *http.Request) interface{} { + var jsonBody struct { + Active bool `json:"active"` + } + + type response struct { + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` + } + + if err := json.NewDecoder(r.Body).Decode(&jsonBody); err != nil { + return response{Success: false, ErrorMessage: err.Error()} + } + accountCode := accountsTypes.Code(mux.Vars(r)["accountCode"]) + if err := handlers.backend.SetAccountActivity(accountCode, jsonBody.Active); err != nil { + return response{Success: false, ErrorMessage: err.Error()} + } + return response{Success: true} +} + func (handlers *Handlers) postSetAccountActive(r *http.Request) interface{} { var jsonBody struct { AccountCode accountsTypes.Code `json:"accountCode"` diff --git a/frontends/web/src/api/account.ts b/frontends/web/src/api/account.ts index cd1b909998..b001ba435c 100644 --- a/frontends/web/src/api/account.ts +++ b/frontends/web/src/api/account.ts @@ -113,6 +113,13 @@ export const getEthAccountCodeAndNameByAddress = (address: string): Promise => { + return apiPost(`account/${code}/activity`, { active }); +}; + export type TStatus = { disabled: boolean; synced: boolean; diff --git a/frontends/web/src/hooks/account-activity.ts b/frontends/web/src/hooks/account-activity.ts new file mode 100644 index 0000000000..6a6a757bb2 --- /dev/null +++ b/frontends/web/src/hooks/account-activity.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect } from 'react'; +import * as accountApi from '@/api/account'; + +const activityRefreshMs = 30_000; + +export const useAccountActivity = ( + code: accountApi.AccountCode, + enabled = true, +) => { + useEffect(() => { + if (!enabled) { + return; + } + + const setActive = (active: boolean) => { + accountApi.setAccountActivity(code, active).catch(console.error); + }; + + const refresh = () => { + if (document.visibilityState === 'visible') { + setActive(true); + } + }; + + const handleVisibilityChange = () => { + setActive(document.visibilityState === 'visible'); + }; + + refresh(); + const intervalID = window.setInterval(refresh, activityRefreshMs); + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + window.clearInterval(intervalID); + document.removeEventListener('visibilitychange', handleVisibilityChange); + setActive(false); + }; + }, [code, enabled]); +}; diff --git a/frontends/web/src/routes/account/account.tsx b/frontends/web/src/routes/account/account.tsx index 15428391cd..a239795bda 100644 --- a/frontends/web/src/routes/account/account.tsx +++ b/frontends/web/src/routes/account/account.tsx @@ -14,6 +14,7 @@ import { GuidedContent, GuideWrapper, Header, Main } from '@/components/layout'; import { Spinner } from '@/components/spinner/Spinner'; import { Message } from '@/components/message/message'; import { useLoad, useSubscribe, useSync } from '@/hooks/api'; +import { useAccountActivity } from '@/hooks/account-activity'; import { useBitsurance } from '@/hooks/bitsurance'; import { useDebounce } from '@/hooks/debounce'; import { useScrollIntoView } from '@/hooks/scroll-into-view'; @@ -22,7 +23,7 @@ import { ActionButtons } from './actionButtons'; import { Insured } from './components/insuredtag'; import { AccountGuide } from './guide'; import { BuyReceiveCTA } from './info/buy-receive-cta'; -import { isBitcoinBased } from './utils'; +import { isBitcoinBased, isEthereumBased } from './utils'; import { MultilineMarkup } from '@/utils/markup'; import { Dialog } from '@/components/dialog/dialog'; import { A } from '@/components/anchor/anchor'; @@ -91,6 +92,9 @@ const RemountAccount = ({ const supportedVendors = useLoad(getMarketVendors(code), [code]); const account = accounts && accounts.find(acct => acct.code === code); + const isEthereumAccount = account !== undefined && isEthereumBased(account.coinCode); + + useAccountActivity(code, isEthereumAccount); const { insured, uncoveredFunds, clearUncoveredFunds } = useBitsurance(code, account); diff --git a/frontends/web/src/routes/account/receive/receive.tsx b/frontends/web/src/routes/account/receive/receive.tsx index 63a64181a6..ef328445cb 100644 --- a/frontends/web/src/routes/account/receive/receive.tsx +++ b/frontends/web/src/routes/account/receive/receive.tsx @@ -4,6 +4,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AppContext } from '@/contexts/AppContext'; import { useLoad } from '@/hooks/api'; +import { useAccountActivity } from '@/hooks/account-activity'; import { UseBackButton } from '@/hooks/backbutton'; import * as accountApi from '@/api/account'; import { setAccountReceiveScriptType } from '@/api/backend'; @@ -139,7 +140,9 @@ export const Receive = ({ const [currentAddressIndex, setCurrentAddressIndex] = useState(0); const account = accounts.find(({ code: accountCode }) => accountCode === code); + const isEthereumAccount = account !== undefined && isEthereumBased(account.coinCode); const insured = account?.bitsuranceStatus === 'active'; + useAccountActivity(code, isEthereumAccount); // first array index: address types. second array index: unused addresses of that address type. const receiveAddresses = useLoad(accountApi.getReceiveAddressList(code));