diff --git a/src/controllers/accounts/accounts.ts b/src/controllers/accounts/accounts.ts index 29231b773..d8a7999ef 100644 --- a/src/controllers/accounts/accounts.ts +++ b/src/controllers/accounts/accounts.ts @@ -91,7 +91,8 @@ export class AccountsController extends EventEmitter { async updateAccountState( accountAddr: Account['addr'], blockTag: 'pending' | 'latest' = 'latest', - networks: NetworkId[] = [] + networks: NetworkId[] = [], + isManualUpdate = false ) { const accountData = this.accounts.find((account) => account.addr === accountAddr) @@ -99,7 +100,7 @@ export class AccountsController extends EventEmitter { await this.withStatus( 'updateAccountState', - async () => this.#updateAccountStates([accountData], blockTag, networks), + async () => this.#updateAccountStates([accountData], blockTag, networks, isManualUpdate), true ) } @@ -107,7 +108,8 @@ export class AccountsController extends EventEmitter { async #updateAccountStates( accounts: Account[], blockTag: string | number = 'latest', - updateOnlyNetworksWithIds: NetworkId[] = [] + updateOnlyNetworksWithIds: NetworkId[] = [], + isManualUpdate = false ) { // if any, update the account state only for the passed networks; else - all const updateOnlyPassedNetworks = updateOnlyNetworksWithIds.length @@ -138,6 +140,17 @@ export class AccountsController extends EventEmitter { }) } catch (err) { console.error(`account state update error for ${network.name}: `, err) + + if (isManualUpdate) { + accounts.forEach((account) => { + const state = this.accountStates?.[account.addr]?.[network.id] + + if (!state) return + // Reset the account state updatedAt timestamp so a banner is displayed + state.updatedAt = 0 + }) + } + this.#updateProviderIsWorking(network.id, false) } @@ -216,4 +229,11 @@ export class AccountsController extends EventEmitter { await this.#storage.set('accounts', this.accounts) this.emitUpdate() } + + get areAccountStatesLoading() { + return ( + this.statuses.updateAccountStates === 'LOADING' || + this.statuses.updateAccountState === 'LOADING' + ) + } } diff --git a/src/controllers/main/main.ts b/src/controllers/main/main.ts index 7fd45353c..641d850af 100644 --- a/src/controllers/main/main.ts +++ b/src/controllers/main/main.ts @@ -588,6 +588,7 @@ export class MainController extends EventEmitter { this.signAccountOp = new SignAccountOpController( this.accounts, + this.providers, this.keystore, this.portfolio, this.#externalSignerControllers, @@ -963,7 +964,12 @@ export class MainController extends EventEmitter { // However, even if we don't trigger an update here, it's not a big problem, // as the account state will be updated anyway, and its update will be very recent. !isUpdatingAccount && this.selectedAccount.account?.addr - ? this.accounts.updateAccountState(this.selectedAccount.account.addr, 'pending') + ? this.accounts.updateAccountState( + this.selectedAccount.account.addr, + 'pending', + undefined, + true + ) : Promise.resolve(), // `updateSelectedAccountPortfolio` doesn't rely on `withStatus` validation internally, // as the PortfolioController already exposes flags that are highly sufficient for the UX. diff --git a/src/controllers/selectedAccount/selectedAccount.ts b/src/controllers/selectedAccount/selectedAccount.ts index a25bb9da2..480fa5cb6 100644 --- a/src/controllers/selectedAccount/selectedAccount.ts +++ b/src/controllers/selectedAccount/selectedAccount.ts @@ -322,12 +322,20 @@ export class SelectedAccountController extends EventEmitter { } #updatePortfolioBanners(skipUpdate?: boolean) { - if (!this.account || !this.#networks || !this.#providers || !this.#portfolio) return + if ( + !this.account || + !this.#networks || + !this.#providers || + !this.#portfolio || + this.#accounts.areAccountStatesLoading + ) + return const networksWithFailedRPCBanners = getNetworksWithFailedRPCBanners({ providers: this.#providers.providers, networks: this.#networks.networks, - networksWithAssets: this.#portfolio.getNetworksWithAssets(this.account.addr) + networksWithAssets: this.#portfolio.getNetworksWithAssets(this.account.addr), + accountState: this.#accounts.accountStates[this.account.addr] }) const errorBanners = getNetworksWithPortfolioErrorBanners({ diff --git a/src/controllers/signAccountOp/signAccountOp.test.ts b/src/controllers/signAccountOp/signAccountOp.test.ts index 4131a3aec..bd8ec9a33 100644 --- a/src/controllers/signAccountOp/signAccountOp.test.ts +++ b/src/controllers/signAccountOp/signAccountOp.test.ts @@ -454,6 +454,7 @@ const init = async ( const callRelayer = relayerCall.bind({ url: '', fetch }) const controller = new SignAccountOpController( accountsCtrl, + providersCtrl, keystore, portfolio, {}, diff --git a/src/controllers/signAccountOp/signAccountOp.ts b/src/controllers/signAccountOp/signAccountOp.ts index 7154641c7..6b43d1f46 100644 --- a/src/controllers/signAccountOp/signAccountOp.ts +++ b/src/controllers/signAccountOp/signAccountOp.ts @@ -59,6 +59,7 @@ import { AccountOpAction } from '../actions/actions' import EventEmitter from '../eventEmitter/eventEmitter' import { KeystoreController } from '../keystore/keystore' import { PortfolioController } from '../portfolio/portfolio' +import { ProvidersController } from '../providers/providers' import { getFeeSpeedIdentifier, getFeeTokenPriceUnavailableWarning, @@ -115,6 +116,8 @@ export class SignAccountOpController extends EventEmitter { #keystore: KeystoreController + #providers: ProvidersController + #portfolio: PortfolioController #externalSignerControllers: ExternalSignerControllers @@ -175,6 +178,7 @@ export class SignAccountOpController extends EventEmitter { constructor( accounts: AccountsController, + providers: ProvidersController, keystore: KeystoreController, portfolio: PortfolioController, externalSignerControllers: ExternalSignerControllers, @@ -189,6 +193,7 @@ export class SignAccountOpController extends EventEmitter { super() this.#accounts = accounts + this.#providers = providers this.#keystore = keystore this.#portfolio = portfolio this.#externalSignerControllers = externalSignerControllers diff --git a/src/interfaces/account.ts b/src/interfaces/account.ts index c1c58e29d..ce4a04217 100644 --- a/src/interfaces/account.ts +++ b/src/interfaces/account.ts @@ -43,6 +43,7 @@ export interface AccountOnchainState { isErc4337Nonce: boolean isV2: boolean currentBlock: bigint + updatedAt: number } export type AccountStates = { diff --git a/src/libs/accountState/accountState.ts b/src/libs/accountState/accountState.ts index c6e32332c..6bcd34913 100644 --- a/src/libs/accountState/accountState.ts +++ b/src/libs/accountState/accountState.ts @@ -82,7 +82,8 @@ export async function getAccountState( ), currentBlock: accResult.currentBlock, deployError: - accounts[index].associatedKeys.length > 0 && accResult.associatedKeyPrivileges.length === 0 + accounts[index].associatedKeys.length > 0 && accResult.associatedKeyPrivileges.length === 0, + updatedAt: Date.now() } return res diff --git a/src/libs/banners/banners.ts b/src/libs/banners/banners.ts index 136021a3f..5396b39c3 100644 --- a/src/libs/banners/banners.ts +++ b/src/libs/banners/banners.ts @@ -1,4 +1,4 @@ -import { Account } from '../../interfaces/account' +import { Account, AccountStates } from '../../interfaces/account' import { AccountOpAction, Action as ActionFromActionsQueue } from '../../interfaces/actions' // eslint-disable-next-line import/no-cycle import { Action, Banner } from '../../interfaces/banner' @@ -327,14 +327,16 @@ export const getKeySyncBanner = (addr: string, email: string, keys: string[]) => export const getNetworksWithFailedRPCBanners = ({ providers, networks, - networksWithAssets + networksWithAssets, + accountState }: { providers: RPCProviders networks: Network[] networksWithAssets: NetworkId[] + accountState: AccountStates[string] }): Banner[] => { const banners: Banner[] = [] - const networkIds = getNetworksWithFailedRPC({ providers }).filter((networkId) => + const networkIds = getNetworksWithFailedRPC({ providers, accountState }).filter((networkId) => networksWithAssets.includes(networkId) ) diff --git a/src/libs/errorHumanizer/errors.ts b/src/libs/errorHumanizer/errors.ts index b11d098ad..070d5d526 100644 --- a/src/libs/errorHumanizer/errors.ts +++ b/src/libs/errorHumanizer/errors.ts @@ -14,7 +14,12 @@ const BROADCAST_OR_ESTIMATION_ERRORS: ErrorHumanizerError[] = [ 'the RPC provider does not support the requested operation. Please check your RPC settings or contact the dApp team.' }, { - reasons: [RPC_HARDCODED_ERRORS.rpcTimeout, 'Unable to connect to provider'], + reasons: [ + RPC_HARDCODED_ERRORS.rpcTimeout, + 'Unable to connect to provider', + 'SERVER_ERROR', + 'NETWORK_ERROR' + ], message: 'of a problem with the RPC on this network. Please try again later, change the RPC or contact support for assistance.' }, diff --git a/src/libs/networks/networks.ts b/src/libs/networks/networks.ts index 7fe332c15..f391e841d 100644 --- a/src/libs/networks/networks.ts +++ b/src/libs/networks/networks.ts @@ -2,6 +2,7 @@ import { AMBIRE_ACCOUNT_FACTORY, OPTIMISTIC_ORACLE, SINGLETON } from '../../consts/deploy' import { networks as predefinedNetworks } from '../../consts/networks' +import { AccountStates } from '../../interfaces/account' import { Fetch } from '../../interfaces/fetch' import { Erc4337settings, @@ -59,11 +60,25 @@ export function is4337Enabled( return hasBundlerSupport } -export const getNetworksWithFailedRPC = ({ providers }: { providers: RPCProviders }): string[] => { - return Object.keys(providers).filter( - (networkId) => - typeof providers[networkId].isWorking === 'boolean' && !providers[networkId].isWorking - ) +export const getNetworksWithFailedRPC = ({ + providers, + accountState +}: { + providers: RPCProviders + accountState: AccountStates[string] +}): string[] => { + return Object.keys(providers).filter((networkId) => { + const isProviderWorking = + typeof providers[networkId].isWorking !== 'boolean' || providers[networkId].isWorking + + if (isProviderWorking) return false + if (!accountState || !accountState[networkId]) return true + + const EIGHT_MINUTES = 1000 * 60 * 8 + const lastUpdate = accountState[networkId]?.updatedAt || 0 + + return Date.now() - lastUpdate > EIGHT_MINUTES + }) } async function retryRequest(init: Function, counter = 0): Promise {