From 46d5387ac4e419f5f4b91dfeca28c606b6d01b43 Mon Sep 17 00:00:00 2001 From: Nuo Xu Date: Thu, 5 Oct 2023 11:42:00 -0700 Subject: [PATCH] Fix #8160: User asset duplication caused by a new iOS17 memory leaks (#8174) --- .../BrowserViewController+Menu.swift | 25 +- .../Settings/SettingsViewController.swift | 8 + Sources/BraveWallet/Crypto/CryptoView.swift | 3 + .../Crypto/Stores/AccountActivityStore.swift | 166 ++++----- .../Crypto/Stores/AssetDetailStore.swift | 124 ++----- .../Crypto/Stores/BuyTokenStore.swift | 88 ++--- .../Crypto/Stores/CryptoStore.swift | 335 ++++++++++-------- .../Crypto/Stores/KeyringStore.swift | 163 +++++---- .../Stores/ManageSiteConnectionsStore.swift | 4 +- .../Crypto/Stores/MarketStore.swift | 3 +- .../Crypto/Stores/NFTDetailStore.swift | 4 +- .../BraveWallet/Crypto/Stores/NFTStore.swift | 137 +++---- .../Crypto/Stores/NetworkSelectionStore.swift | 4 +- .../Crypto/Stores/NetworkStore.swift | 164 ++++----- .../Crypto/Stores/PortfolioStore.swift | 154 ++++---- .../Stores/SelectAccountTokenStore.swift | 64 ++-- .../Crypto/Stores/SendTokenStore.swift | 104 +++--- .../Crypto/Stores/SettingsStore.swift | 108 ++---- .../Crypto/Stores/SwapTokenStore.swift | 135 +++---- .../Crypto/Stores/TabDappStore.swift | 4 +- .../Stores/TransactionConfirmationStore.swift | 160 ++++----- .../Stores/TransactionDetailsStore.swift | 4 +- .../Stores/TransactionsActivityStore.swift | 134 +++---- .../Crypto/Stores/UserAssetsStore.swift | 110 +++--- .../Crypto/Stores/WalletStore.swift | 59 +++ .../Crypto/WalletServiceObservers.swift | 223 ++++++++++++ .../Panels/RequestContainerView.swift | 3 + .../Preview Content/MockStores.swift | 1 + .../WalletHostingViewController.swift | 8 + .../WalletPanelHostingController.swift | 8 + .../SendTokenStoreTests.swift | 1 + .../UserAssetsStoreTests.swift | 13 +- 32 files changed, 1285 insertions(+), 1236 deletions(-) create mode 100644 Sources/BraveWallet/Crypto/WalletServiceObservers.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift index e858a76cd6b..4706260789e 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift @@ -162,18 +162,23 @@ extension BrowserViewController { let walletService = BraveWallet.ServiceFactory.get(privateMode: isPrivateMode) let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode) - var keyringStore: KeyringStore? - if let keyringService = keyringService, - let walletService = walletService, - let rpcService = rpcService { - keyringStore = KeyringStore( - keyringService: keyringService, - walletService: walletService, - rpcService: rpcService - ) + var keyringStore: KeyringStore? = walletStore?.keyringStore + if keyringStore == nil { + if let keyringService = keyringService, + let walletService = walletService, + let rpcService = rpcService { + keyringStore = KeyringStore( + keyringService: keyringService, + walletService: walletService, + rpcService: rpcService + ) + } } - let cryptoStore = CryptoStore.from(ipfsApi: braveCore.ipfsAPI, privateMode: isPrivateMode) + var cryptoStore: CryptoStore? = walletStore?.cryptoStore + if cryptoStore == nil { + cryptoStore = CryptoStore.from(ipfsApi: braveCore.ipfsAPI, privateMode: isPrivateMode) + } let vc = SettingsViewController( profile: self.profile, diff --git a/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/Sources/Brave/Frontend/Settings/SettingsViewController.swift index 043e7b8edd0..c4b4bd23e2a 100644 --- a/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -91,6 +91,11 @@ class SettingsViewController: TableViewController { super.init(style: .insetGrouped) } + + deinit { + keyringStore?.tearDown() + cryptoStore?.tearDown() + } @available(*, unavailable) required init?(coder aDecoder: NSCoder) { @@ -841,6 +846,9 @@ class SettingsViewController: TableViewController { Row( text: Strings.Wallet.web3, selection: { [unowned self] in + // iOS17 memory leak issue #8160 + keyringStore?.setupObservers() + cryptoStore?.setupObservers() let web3SettingsView = Web3SettingsView( settingsStore: settingsStore, networkStore: cryptoStore?.networkStore, diff --git a/Sources/BraveWallet/Crypto/CryptoView.swift b/Sources/BraveWallet/Crypto/CryptoView.swift index 0f42cfbda18..12b0d0d928d 100644 --- a/Sources/BraveWallet/Crypto/CryptoView.swift +++ b/Sources/BraveWallet/Crypto/CryptoView.swift @@ -137,6 +137,9 @@ public struct CryptoView: View { ), networkStore: store.networkStore ) + .onDisappear { + store.closeAccountActivityStore(for: walletStore.keyringStore.selectedAccount) + } .toolbar { dismissButtonToolbarContents } diff --git a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift index 8369fa7574a..565d73c2155 100644 --- a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift @@ -6,7 +6,7 @@ import Foundation import BraveCore -class AccountActivityStore: ObservableObject { +class AccountActivityStore: ObservableObject, WalletObserverStore { /// If we want to observe selected account changes (ex. in `WalletPanelView`). /// In some cases, we do not want to update the account displayed when the /// selected account changes (ex. when removing an account). @@ -37,7 +37,16 @@ class AccountActivityStore: ObservableObject { /// Cache for storing `BlockchainToken`s that are not in user assets or our token registry. /// This could occur with a dapp creating a transaction. private var tokenInfoCache: [String: BraveWallet.BlockchainToken] = [:] - + + private var keyringServiceObserver: KeyringServiceObserver? + private var rpcServiceObserver: JsonRpcServiceObserver? + private var txServiceObserver: TxServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && rpcServiceObserver != nil && txServiceObserver != nil && walletServiceObserver != nil + } + init( account: BraveWallet.AccountInfo, observeAccountUpdates: Bool, @@ -63,15 +72,60 @@ class AccountActivityStore: ObservableObject { self.ipfsApi = ipfsApi self.assetManager = userAssetManager - self.keyringService.add(self) - self.rpcService.add(self) - self.txService.add(self) - self.walletService.add(self) + self.setupObservers() walletService.defaultBaseCurrency { [self] currencyCode in self.currencyCode = currencyCode } } + + func tearDown() { + keyringServiceObserver = nil + rpcServiceObserver = nil + txServiceObserver = nil + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _selectedWalletAccountChanged: { [weak self] account in + guard let self, self.observeAccountUpdates else { return } + self.account = account + self.update() + }, + _selectedDappAccountChanged: { [weak self] _, account in + guard let self, self.observeAccountUpdates, let account else { return } + self.account = account + self.update() + } + ) + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, _, _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // Handle small gap between chain changing and txController having the correct chain Id + self?.update() + } + } + ) + self.txServiceObserver = TxServiceObserver( + txService: txService, + _onNewUnapprovedTx: { [weak self] _ in + self?.update() + }, + _onTransactionStatusChanged: { [weak self] _ in + self?.update() + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onDefaultBaseCurrencyChanged: { [weak self] currency in + self?.currencyCode = currency + } + ) + } func update() { Task { @MainActor in @@ -273,103 +327,3 @@ class AccountActivityStore: ObservableObject { } #endif } - -extension AccountActivityStore: BraveWalletKeyringServiceObserver { - func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - func keyringReset() { - } - - func locked() { - } - - func unlocked() { - } - - func backedUp() { - } - - func accountsChanged() { - } - - func autoLockMinutesChanged() { - } - - func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - guard observeAccountUpdates else { return } - self.account = account - update() - } - - func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - guard observeAccountUpdates, let account else { return } - self.account = account - update() - } - - func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension AccountActivityStore: BraveWalletJsonRpcServiceObserver { - func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // Handle small gap between chain changing and txController having the correct chain Id - self.update() - } - } - func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } -} - -extension AccountActivityStore: BraveWalletTxServiceObserver { - func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) { - update() - } - func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) { - update() - } - func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) { - } - func onTxServiceReset() { - } -} - -extension AccountActivityStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - currencyCode = currency - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - } - - func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDiscoverAssetsStarted() { - } - - func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { - } - - func onResetWallet() { - } -} diff --git a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift index 36d08dcb514..42c7a949985 100644 --- a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift @@ -32,7 +32,7 @@ enum AssetDetailType: Identifiable { } } -class AssetDetailStore: ObservableObject { +class AssetDetailStore: ObservableObject, WalletObserverStore { @Published private(set) var isInitialState: Bool = true @Published private(set) var isLoadingPrice: Bool = false @Published private(set) var isLoadingChart: Bool = false @@ -81,6 +81,9 @@ class AssetDetailStore: ObservableObject { /// A list of tokens that are supported with the current selected network for all supported /// on-ramp providers. private var allBuyTokensAllOptions: [BraveWallet.OnRampProvider: [BraveWallet.BlockchainToken]] = [:] + private var keyringServiceObserver: KeyringServiceObserver? + private var txServiceObserver: TxServiceObserver? + private var walletServiceObserver: WalletServiceObserver? let assetDetailType: AssetDetailType var assetDetailToken: BraveWallet.BlockchainToken { switch assetDetailType { @@ -103,6 +106,10 @@ class AssetDetailStore: ObservableObject { } } } + + var isObserving: Bool { + keyringServiceObserver != nil && txServiceObserver != nil && walletServiceObserver != nil + } init( assetRatioService: BraveWalletAssetRatioService, @@ -127,14 +134,40 @@ class AssetDetailStore: ObservableObject { self.assetManager = userAssetManager self.assetDetailType = assetDetailType - self.keyringService.add(self) - self.txService.add(self) - self.walletService.add(self) - + self.setupObservers() + walletService.defaultBaseCurrency { [self] currencyCode in self.currencyCode = currencyCode } } + + func tearDown() { + keyringServiceObserver = nil + txServiceObserver = nil + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _accountsChanged: { [weak self] in + self?.update() + } + ) + self.txServiceObserver = TxServiceObserver( + txService: txService, + _onTransactionStatusChanged: { [weak self] _ in + self?.update() + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onDefaultBaseCurrencyChanged: { [weak self] currency in + self?.currencyCode = currency + } + ) + } private let percentFormatter = NumberFormatter().then { $0.numberStyle = .percent @@ -370,84 +403,3 @@ class AssetDetailStore: ObservableObject { } } } - -extension AssetDetailStore: BraveWalletKeyringServiceObserver { - func keyringReset() { - } - - func accountsChanged() { - update() - } - - func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - func locked() { - } - - func unlocked() { - } - - func backedUp() { - } - - func autoLockMinutesChanged() { - } - - func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - } - - func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension AssetDetailStore: BraveWalletTxServiceObserver { - func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) { - } - func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) { - } - func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) { - update() - } - func onTxServiceReset() { - } -} - -extension AssetDetailStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - currencyCode = currency - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - } - - func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - func onDiscoverAssetsStarted() { - } - - func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { - } - - func onResetWallet() { - } -} diff --git a/Sources/BraveWallet/Crypto/Stores/BuyTokenStore.swift b/Sources/BraveWallet/Crypto/Stores/BuyTokenStore.swift index 6bb8ffa0ddc..15434bbc4df 100644 --- a/Sources/BraveWallet/Crypto/Stores/BuyTokenStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/BuyTokenStore.swift @@ -9,7 +9,7 @@ import OrderedCollections import Combine /// A store contains data for buying tokens -public class BuyTokenStore: ObservableObject { +public class BuyTokenStore: ObservableObject, WalletObserverStore { /// The current selected token to buy. Default with nil value. @Published var selectedBuyToken: BraveWallet.BlockchainToken? /// The supported currencies for purchasing @@ -56,6 +56,8 @@ public class BuyTokenStore: ObservableObject { private var selectedNetwork: BraveWallet.NetworkInfo = .init() private(set) var orderedSupportedBuyOptions: OrderedSet = [] private var prefilledToken: BraveWallet.BlockchainToken? + private var rpcServiceObserver: JsonRpcServiceObserver? + private var keyringServiceObserver: KeyringServiceObserver? /// A map between chain id and gas token's symbol static let gasTokens: [String: [String]] = [ @@ -70,6 +72,10 @@ public class BuyTokenStore: ObservableObject { BraveWallet.FilecoinMainnet: ["fil"], BraveWallet.AvalancheMainnetChainId: ["avax", "avaxc"] ] + + var isObserving: Bool { + rpcServiceObserver != nil && keyringServiceObserver != nil + } public init( blockchainRegistry: BraveWalletBlockchainRegistry, @@ -91,14 +97,38 @@ public class BuyTokenStore: ObservableObject { $0[$1] = [] } - self.rpcService.add(self) - self.keyringService.add(self) + self.setupObservers() Task { await updateInfo() } } + func tearDown() { + rpcServiceObserver = nil + keyringServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, _, _ in + Task { [self] in + await self?.updateInfo() + } + } + ) + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _selectedWalletAccountChanged: { [weak self] _ in + Task { @MainActor [self] in + await self?.updateInfo() + } + } + ) + } + @MainActor private func validatePrefilledToken(on network: BraveWallet.NetworkInfo) async { guard let prefilledToken = self.prefilledToken else { return @@ -226,58 +256,6 @@ public class BuyTokenStore: ObservableObject { } } -extension BuyTokenStore: BraveWalletJsonRpcServiceObserver { - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - Task { - await updateInfo() - } - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } -} - -extension BuyTokenStore: BraveWalletKeyringServiceObserver { - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringReset() { - } - - public func locked() { - } - - public func unlocked() { - } - - public func backedUp() { - } - - public func accountsChanged() { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } - - public func autoLockMinutesChanged() { - } - - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - Task { @MainActor in - await updateInfo() - } - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } -} - private extension BraveWallet.BlockchainToken { var isGasToken: Bool { guard let gasTokensByChain = BuyTokenStore.gasTokens[chainId] else { return false } diff --git a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift index dc213159b26..e9bdffaeaea 100644 --- a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift @@ -57,7 +57,7 @@ enum WebpageRequestResponse: Equatable { case signAllTransactions(approved: Bool, id: Int32) } -public class CryptoStore: ObservableObject { +public class CryptoStore: ObservableObject, WalletObserverStore { public let networkStore: NetworkStore public let portfolioStore: PortfolioStore let nftStore: NFTStore @@ -102,6 +102,11 @@ public class CryptoStore: ObservableObject { } } + /// A boolean value indicate this class is observing wallet service changes. + public var isObserving: Bool { + keyringServiceObserver != nil && rpcServiceObserver != nil && txServiceObserver != nil && walletServiceObserver != nil + } + private let keyringService: BraveWalletKeyringService private let rpcService: BraveWalletJsonRpcService private let walletService: BraveWalletBraveWalletService @@ -116,6 +121,11 @@ public class CryptoStore: ObservableObject { private var isUpdatingUserAssets: Bool = false private var autoDiscoveredAssets: [BraveWallet.BlockchainToken] = [] + private var keyringServiceObserver: KeyringServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + private var txServiceObserver: TxServiceObserver? + private var rpcServiceObserver: JsonRpcServiceObserver? + public init( keyringService: BraveWalletKeyringService, rpcService: BraveWalletJsonRpcService, @@ -185,10 +195,7 @@ public class CryptoStore: ObservableObject { walletService: walletService ) - self.keyringService.add(self) - self.txService.add(self) - self.rpcService.add(self) - self.walletService.add(self) + setupObservers() Preferences.Wallet.migrateCoreToWalletUserAssetCompleted.observe(from: self) @@ -199,6 +206,149 @@ public class CryptoStore: ObservableObject { } } + public func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _keyringReset: { [weak self] in + WalletProviderPermissionRequestsManager.shared.cancelAllPendingRequests(for: [.eth, .sol]) + WalletProviderAccountCreationRequestManager.shared.cancelAllPendingRequests(coins: [.eth, .sol]) + self?.rejectAllPendingWebpageRequests() + }, + _keyringCreated: { _ in + // 1. We don't need to rely on this observer method to migrate user visible assets + // when user creates a new wallet, since in this case `CryptoStore` has not yet been initialized + // 2. We don't need to rely on this observer method to migrate user visible assets + // when user creates or imports a new account with a new keyring since any new + // supported coin type / keyring will be migrated inside `CryptoStore`'s init() + }, + _keyringRestored: { [weak self] _ in + // This observer method will only get called when user restore a wallet + // from the lock screen + // We will need to + // 1. reset wallet user asset migration flag + // 2. wipe user assets local storage + // 3. migrate user assets with new keyring + guard let isUpdatingUserAssets = self?.isUpdatingUserAssets, !isUpdatingUserAssets else { return } + self?.isUpdatingUserAssets = true + Preferences.Wallet.migrateCoreToWalletUserAssetCompleted.reset() + WalletUserAssetGroup.removeAllGroup() { + self?.userAssetManager.migrateUserAssets(completion: { + self?.updateAssets() + self?.isUpdatingUserAssets = false + }) + } + }, + _locked: { [weak self] in + self?.isPresentingPendingRequest = false + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onNetworkListChanged: { [weak self] in + self?.updateAssets() + }, + _onDiscoverAssetsCompleted: { [weak self] discoveredAssets in + // Failsafe incase two CryptoStore's are initialized (see brave-ios #7804) and asset + // migration is slow. Makes sure auto-discovered assets during asset migration to + // CoreData are added after. + guard let self else { return } + if !self.isUpdatingUserAssets { + for asset in discoveredAssets { + self.userAssetManager.addUserAsset(asset, completion: nil) + } + if !discoveredAssets.isEmpty { + self.updateAssets() + } + } else { + self.autoDiscoveredAssets.append(contentsOf: discoveredAssets) + } + } + ) + self.txServiceObserver = TxServiceObserver( + txService: txService, + _onNewUnapprovedTx: { [weak self] _ in + self?.prepare() + }, + _onUnapprovedTxUpdated: { [weak self] _ in + self?.prepare() + }, + _onTransactionStatusChanged: { [weak self] _ in + self?.prepare() + }, + _onTxServiceReset: { [weak self] in + self?.prepare() + } + ) + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, _, _ in + // if user had just changed networks, there is a potential race condition + // blocking presenting pendingRequest here, as Network Selection might still be on screen + // by delaying here instead of at present we only delay after chain changes (#6750) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self?.prepare() + } + }, + _onAddEthereumChainRequestCompleted: { [weak self] chainId, error in + Task { @MainActor [self] in + if let addNetworkDappRequestCompletion = self?.addNetworkDappRequestCompletion[chainId] { + if error.isEmpty { + let allNetworks = await self?.rpcService.allNetworks(.eth) + if let network = allNetworks?.first(where: { $0.chainId == chainId }) { + self?.userAssetManager.addUserAsset(network.nativeToken) { + self?.updateAssets() + } + } + } + addNetworkDappRequestCompletion(error.isEmpty ? nil : error) + self?.addNetworkDappRequestCompletion[chainId] = nil + } + } + } + ) + + // sub stores' observers + networkStore.setupObservers() + portfolioStore.setupObservers() + nftStore.setupObservers() + transactionsActivityStore.setupObservers() + marketStore.setupObservers() + settingsStore.setupObservers() + + accountActivityStore?.setupObservers() + assetDetailStore?.setupObservers() + nftDetailStore?.setupObservers() + confirmationStore?.setupObservers() + buyTokenStore?.setupObservers() + sendTokenStore?.setupObservers() + swapTokenStore?.setupObservers() + } + + // A manual tear-down that nil all the wallet service observer classes + public func tearDown() { + keyringServiceObserver = nil + walletServiceObserver = nil + txServiceObserver = nil + rpcServiceObserver = nil + + // sub-stores + networkStore.tearDown() + portfolioStore.tearDown() + nftStore.tearDown() + transactionsActivityStore.tearDown() + marketStore.tearDown() + settingsStore.tearDown() + + accountActivityStore?.tearDown() + assetDetailStore?.tearDown() + nftDetailStore?.tearDown() + confirmationStore?.tearDown() + buyTokenStore?.tearDown() + sendTokenStore?.tearDown() + swapTokenStore?.tearDown() + } + private var buyTokenStore: BuyTokenStore? func openBuyTokenStore(_ prefilledToken: BraveWallet.BlockchainToken?) -> BuyTokenStore { if let store = buyTokenStore { @@ -259,6 +409,15 @@ public class CryptoStore: ObservableObject { return store } + func closeBSSStores() { + buyTokenStore?.tearDown() + sendTokenStore?.tearDown() + swapTokenStore?.tearDown() + buyTokenStore = nil + sendTokenStore = nil + swapTokenStore = nil + } + private var assetDetailStore: AssetDetailStore? func assetDetailStore(for assetDetailType: AssetDetailType) -> AssetDetailStore { if let store = assetDetailStore, store.assetDetailType.id == assetDetailType.id { @@ -282,6 +441,7 @@ public class CryptoStore: ObservableObject { func closeAssetDetailStore(for assetDetailType: AssetDetailType) { if let store = assetDetailStore, store.assetDetailType.id == assetDetailType.id { + assetDetailStore?.tearDown() assetDetailStore = nil } } @@ -315,16 +475,11 @@ public class CryptoStore: ObservableObject { func closeAccountActivityStore(for account: BraveWallet.AccountInfo) { if let store = accountActivityStore, store.account.address == account.address { + accountActivityStore?.tearDown() accountActivityStore = nil } } - func closeBSSStores() { - buyTokenStore = nil - sendTokenStore = nil - swapTokenStore = nil - } - private var confirmationStore: TransactionConfirmationStore? func openConfirmationStore() -> TransactionConfirmationStore { if let store = confirmationStore { @@ -345,13 +500,10 @@ public class CryptoStore: ObservableObject { return store } - public private(set) lazy var settingsStore = SettingsStore( - keyringService: keyringService, - walletService: walletService, - rpcService: rpcService, - txService: txService, - ipfsApi: ipfsApi - ) + func closeConfirmationStore() { + confirmationStore?.tearDown() + confirmationStore = nil + } private var nftDetailStore: NFTDetailStore? func nftDetailStore(for nft: BraveWallet.BlockchainToken, nftMetadata: NFTMetadata?) -> NFTDetailStore { @@ -370,10 +522,19 @@ public class CryptoStore: ObservableObject { func closeNFTDetailStore(for nft: BraveWallet.BlockchainToken) { if let store = nftDetailStore, store.nft.id == nft.id { + nftDetailStore?.tearDown() nftDetailStore = nil } } + public private(set) lazy var settingsStore = SettingsStore( + keyringService: keyringService, + walletService: walletService, + rpcService: rpcService, + txService: txService, + ipfsApi: ipfsApi + ) + // This will be called when users exit from edit visible asset screen // so that Portfolio and NFT tabs will update assets func updateAssets() { @@ -547,144 +708,6 @@ public class CryptoStore: ObservableObject { } } -extension CryptoStore: BraveWalletTxServiceObserver { - public func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) { - prepare() - } - public func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) { - prepare() - } - public func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) { - prepare() - } - public func onTxServiceReset() { - prepare() - } -} - -extension CryptoStore: BraveWalletKeyringServiceObserver { - public func keyringReset() { - WalletProviderPermissionRequestsManager.shared.cancelAllPendingRequests(for: [.eth, .sol]) - WalletProviderAccountCreationRequestManager.shared.cancelAllPendingRequests(coins: [.eth, .sol]) - rejectAllPendingWebpageRequests() - } - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - // 1. We don't need to rely on this observer method to migrate user visible assets - // when user creates a new wallet, since in this case `CryptoStore` has not yet been initialized - // 2. We don't need to rely on this observer method to migrate user visible assets - // when user creates or imports a new account with a new keyring since any new - // supported coin type / keyring will be migrated inside `CryptoStore`'s init() - } - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - // This observer method will only get called when user restore a wallet - // from the lock screen - // We will need to - // 1. reset wallet user asset migration flag - // 2. wipe user assets local storage - // 3. migrate user assets with new keyring - guard !isUpdatingUserAssets else { return } - isUpdatingUserAssets = true - Preferences.Wallet.migrateCoreToWalletUserAssetCompleted.reset() - WalletUserAssetGroup.removeAllGroup() { [weak self] in - self?.userAssetManager.migrateUserAssets(completion: { - self?.updateAssets() - self?.isUpdatingUserAssets = false - }) - } - } - public func locked() { - isPresentingPendingRequest = false - } - public func unlocked() { - } - public func backedUp() { - } - public func accountsChanged() { - } - public func autoLockMinutesChanged() { - } - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - } - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension CryptoStore: BraveWalletJsonRpcServiceObserver { - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - // if user had just changed networks, there is a potential race condition - // blocking presenting pendingRequest here, as Network Selection might still be on screen - // by delaying here instead of at present we only delay after chain changes (#6750) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.prepare() - } - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - Task { @MainActor in - if let addNetworkDappRequestCompletion = addNetworkDappRequestCompletion[chainId] { - if error.isEmpty { - let allNetworks = await rpcService.allNetworks(.eth) - if let network = allNetworks.first(where: { $0.chainId == chainId }) { - userAssetManager.addUserAsset(network.nativeToken) { [weak self] in - self?.updateAssets() - } - } - } - addNetworkDappRequestCompletion(error.isEmpty ? nil : error) - self.addNetworkDappRequestCompletion[chainId] = nil - } - } - } - - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } -} - -extension CryptoStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - updateAssets() - } - - public func onDiscoverAssetsStarted() { - } - - public func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { - // Failsafe incase two CryptoStore's are initialized (see brave-ios #7804) and asset - // migration is slow. Makes sure auto-discovered assets during asset migration to - // CoreData are added after. - if !isUpdatingUserAssets { - for asset in discoveredAssets { - userAssetManager.addUserAsset(asset, completion: nil) - } - if !discoveredAssets.isEmpty { - updateAssets() - } - } else { - autoDiscoveredAssets.append(contentsOf: discoveredAssets) - } - } - - public func onResetWallet() { - } -} - extension CryptoStore: PreferencesObserver { public func preferencesDidChange(for key: String) { // we are only observing `Preferences.Wallet.migrateCoreToWalletUserAssetCompleted` diff --git a/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift b/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift index d7badf73f87..4217f26dfee 100644 --- a/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift @@ -113,7 +113,7 @@ enum PasswordStatus: Equatable { /// An interface that helps you interact with a users keyring /// /// This wraps a KeyringService that you would obtain through BraveCore and makes it observable -public class KeyringStore: ObservableObject { +public class KeyringStore: ObservableObject, WalletObserverStore { /// The defualt keyring information. By default this is an empty keyring which has no accounts. @Published private(set) var defaultKeyring: BraveWallet.KeyringInfo = .init( id: .default, @@ -178,6 +178,12 @@ public class KeyringStore: ObservableObject { private let rpcService: BraveWalletJsonRpcService private var cancellable: AnyCancellable? private let keychain: KeychainType + private var keyringServiceObserver: KeyringServiceObserver? + private var rpcServiceObserver: JsonRpcServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && rpcServiceObserver != nil + } public init( keyringService: BraveWalletKeyringService, @@ -190,8 +196,8 @@ public class KeyringStore: ObservableObject { self.rpcService = rpcService self.keychain = keychain - keyringService.add(self) - rpcService.add(self) + self.setupObservers() + updateKeyringInfo() self.keyringService.keyringInfo(BraveWallet.KeyringId.default) { [self] keyringInfo in @@ -210,6 +216,77 @@ public class KeyringStore: ObservableObject { self?.updateKeyringInfo() } } + + public func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _keyringReset: { [weak self] in + self?.isOnboardingVisible = true + self?.updateKeyringInfo() + }, + _keyringCreated: { [weak self] keyringId in + guard let self else { return } + if self.isOnboardingVisible, !self.isCreatingWallet, keyringId == BraveWallet.KeyringId.default { + // Another window has created a wallet. We should dismiss onboarding on this + // window and allow the other window to continue with it's onboarding flow. + self.isOnboardingVisible = false + } + + Task { @MainActor in + let newKeyring = await self.keyringService.keyringInfo(keyringId) + let selectedAccount = await self.keyringService.allAccounts().selectedAccount + // if the new Keyring doesn't have a selected account, select the first account + if selectedAccount == nil, let newAccount = newKeyring.accountInfos.first { + await self.keyringService.setSelectedAccount(newAccount.accountId) + } + self.updateKeyringInfo() + } + }, + _keyringRestored: { [weak self] keyringId in + guard let self else { return } + if self.isOnboardingVisible && !self.isRestoringWallet, keyringId == BraveWallet.KeyringId.default { + // Another window has restored a wallet. We should dismiss onboarding on this + // window and allow the other window to continue with it's onboarding flow. + self.isOnboardingVisible = false + } + + self.updateKeyringInfo() + }, + _locked: { [weak self] in + // Put this in the background since biometrics prompt will block the main queue + DispatchQueue.main.async { + self?.updateKeyringInfo() + } + }, + _unlocked: { [weak self] in + self?.updateKeyringInfo() + }, + _backedUp: { [weak self] in + self?.updateKeyringInfo() + }, + _accountsChanged: { [weak self] in + self?.updateKeyringInfo() + }, + _selectedWalletAccountChanged: { [weak self] _ in + self?.updateKeyringInfo() + }, + _selectedDappAccountChanged: { [weak self] _, _ in + self?.updateKeyringInfo() + } + ) + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, _, _ in + self?.updateKeyringInfo() + } + ) + } + + public func tearDown() { + keyringServiceObserver = nil + rpcServiceObserver = nil + } private func updateKeyringInfo() { if UIApplication.shared.applicationState != .active { @@ -488,83 +565,3 @@ public class KeyringStore: ObservableObject { keychain.getPasswordFromKeychain(key: Self.passwordKeychainKey) } } - -extension KeyringStore: BraveWalletKeyringServiceObserver { - public func keyringReset() { - isOnboardingVisible = true - updateKeyringInfo() - } - - public func autoLockMinutesChanged() { - } - - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - updateKeyringInfo() - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - updateKeyringInfo() - } - - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - if isOnboardingVisible, !isCreatingWallet, keyringId == BraveWallet.KeyringId.default { - // Another window has created a wallet. We should dismiss onboarding on this - // window and allow the other window to continue with it's onboarding flow. - isOnboardingVisible = false - } - - Task { @MainActor in - let newKeyring = await keyringService.keyringInfo(keyringId) - let selectedAccount = await keyringService.allAccounts().selectedAccount - // if the new Keyring doesn't have a selected account, select the first account - if selectedAccount == nil, let newAccount = newKeyring.accountInfos.first { - await keyringService.setSelectedAccount(newAccount.accountId) - } - updateKeyringInfo() - } - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - if isOnboardingVisible && !isRestoringWallet, keyringId == BraveWallet.KeyringId.default { - // Another window has restored a wallet. We should dismiss onboarding on this - // window and allow the other window to continue with it's onboarding flow. - isOnboardingVisible = false - } - - updateKeyringInfo() - } - - public func locked() { - // Put this in the background since biometrics prompt will block the main queue - DispatchQueue.main.async { [self] in - self.updateKeyringInfo() - } - } - - public func unlocked() { - updateKeyringInfo() - } - - public func backedUp() { - updateKeyringInfo() - } - - public func accountsChanged() { - updateKeyringInfo() - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension KeyringStore: BraveWalletJsonRpcServiceObserver { - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - updateKeyringInfo() - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } -} diff --git a/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift b/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift index d189197781e..cb03e7da203 100644 --- a/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift @@ -29,11 +29,13 @@ extension Array where Element == SiteConnection { } } -class ManageSiteConnectionsStore: ObservableObject { +class ManageSiteConnectionsStore: ObservableObject, WalletObserverStore { @Published var siteConnections: [SiteConnection] = [] var keyringStore: KeyringStore + var isObserving: Bool = false + init(keyringStore: KeyringStore) { self.keyringStore = keyringStore } diff --git a/Sources/BraveWallet/Crypto/Stores/MarketStore.swift b/Sources/BraveWallet/Crypto/Stores/MarketStore.swift index 152a1b85fac..b303363d7e3 100644 --- a/Sources/BraveWallet/Crypto/Stores/MarketStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/MarketStore.swift @@ -14,7 +14,7 @@ struct CoinViewModel: Identifiable { var priceChangePercentage24h: String } -public class MarketStore: ObservableObject { +public class MarketStore: ObservableObject, WalletObserverStore { /// Avalaible coins in market @Published var coins: [BraveWallet.CoinMarket] = [] /// Currency code for prices @@ -39,6 +39,7 @@ public class MarketStore: ObservableObject { $0.minimumFractionDigits = 2 $0.roundingMode = .up } + var isObserving: Bool = false init( assetRatioService: BraveWalletAssetRatioService, diff --git a/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift index c4ccac5edb2..8176debae24 100644 --- a/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift @@ -48,13 +48,15 @@ struct NFTMetadata: Codable, Equatable { } } -class NFTDetailStore: ObservableObject { +class NFTDetailStore: ObservableObject, WalletObserverStore { private let rpcService: BraveWalletJsonRpcService private let ipfsApi: IpfsAPI let nft: BraveWallet.BlockchainToken @Published var isLoading: Bool = false @Published var nftMetadata: NFTMetadata? @Published var networkInfo: BraveWallet.NetworkInfo = .init() + + var isObserving: Bool = false init( rpcService: BraveWalletJsonRpcService, diff --git a/Sources/BraveWallet/Crypto/Stores/NFTStore.swift b/Sources/BraveWallet/Crypto/Stores/NFTStore.swift index baee7d745d1..1c21bc43dca 100644 --- a/Sources/BraveWallet/Crypto/Stores/NFTStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NFTStore.swift @@ -23,7 +23,7 @@ struct NFTAssetViewModel: Identifiable, Equatable { } } -public class NFTStore: ObservableObject { +public class NFTStore: ObservableObject, WalletObserverStore { /// The users visible NFTs @Published private(set) var userVisibleNFTs: [NFTAssetViewModel] = [] /// All User Accounts @@ -58,6 +58,7 @@ public class NFTStore: ObservableObject { rpcService: self.rpcService, keyringService: self.keyringService, assetRatioService: self.assetRatioService, + walletService: self.walletService, ipfsApi: self.ipfsApi, userAssetManager: self.assetManager ) @@ -69,12 +70,19 @@ public class NFTStore: ObservableObject { private let blockchainRegistry: BraveWalletBlockchainRegistry private let ipfsApi: IpfsAPI private let assetManager: WalletUserAssetManagerType + private var rpcServiceObserver: JsonRpcServiceObserver? + private var keyringServiceObserver: KeyringServiceObserver? + private var walletServiveObserber: WalletServiceObserver? /// Cancellable for the last running `update()` Task. private var updateTask: Task<(), Never>? /// Cache of metadata for NFTs. The key is the token's `id`. private var metadataCache: [String: NFTMetadata] = [:] + var isObserving: Bool { + rpcServiceObserver != nil && keyringServiceObserver != nil && walletServiveObserber != nil + } + public init( keyringService: BraveWalletKeyringService, rpcService: BraveWalletJsonRpcService, @@ -92,9 +100,7 @@ public class NFTStore: ObservableObject { self.ipfsApi = ipfsApi self.assetManager = userAssetManager - self.rpcService.add(self) - self.keyringService.add(self) - self.walletService.add(self) + self.setupObservers() keyringService.isLocked { [self] isLocked in if !isLocked { @@ -107,6 +113,51 @@ public class NFTStore: ObservableObject { Preferences.Wallet.nonSelectedNetworksFilter.observe(from: self) } + func tearDown() { + rpcServiceObserver = nil + keyringServiceObserver = nil + walletServiveObserber = nil + + userAssetsStore.tearDown() + } + + func setupObservers() { + guard !isObserving else { return } + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, _, _ in + self?.update() + } + ) + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _unlocked: { [weak self] in + DispatchQueue.main.async { + self?.update() + } + }, + _accountsChanged: { [weak self] in + self?.update() + } + ) + self.walletServiveObserber = WalletServiceObserver( + walletService: walletService, + _onNetworkListChanged: { [weak self] in + // A network was added or removed, `update()` will update `allNetworks`. + self?.update() + }, + _onDiscoverAssetsStarted: { [weak self] in + self?.isLoadingDiscoverAssets = true + }, + _onDiscoverAssetsCompleted: { [weak self] _ in + self?.isLoadingDiscoverAssets = false + // assets update will be called via `CryptoStore` + } + ) + + userAssetsStore.setupObservers() + } + /// Cache of NFT balances for each account tokenBalances: [token.contractAddress] private var nftBalancesCache: [String: [String: Int]] = [:] @@ -231,84 +282,6 @@ public class NFTStore: ObservableObject { } } -extension NFTStore: BraveWalletJsonRpcServiceObserver { - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - update() - } -} - -extension NFTStore: BraveWalletKeyringServiceObserver { - public func keyringReset() { - } - - public func accountsChanged() { - update() - } - public func backedUp() { - } - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - public func locked() { - } - public func unlocked() { - DispatchQueue.main.async { [self] in - update() - } - } - public func autoLockMinutesChanged() { - } - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension NFTStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - // A network was added or removed, `update()` will update `allNetworks`. - update() - } - - public func onDiscoverAssetsStarted() { - isLoadingDiscoverAssets = true - } - - public func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { - isLoadingDiscoverAssets = false - // assets update will be called via `CryptoStore` - } - - public func onResetWallet() { - } -} - extension NFTStore: PreferencesObserver { func saveFilters(_ filters: Filters) { isSavingFilters = true diff --git a/Sources/BraveWallet/Crypto/Stores/NetworkSelectionStore.swift b/Sources/BraveWallet/Crypto/Stores/NetworkSelectionStore.swift index 929b86006ce..1b04351f0fe 100644 --- a/Sources/BraveWallet/Crypto/Stores/NetworkSelectionStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NetworkSelectionStore.swift @@ -7,7 +7,7 @@ import BraveCore import BraveShared import SwiftUI -class NetworkSelectionStore: ObservableObject { +class NetworkSelectionStore: ObservableObject, WalletObserverStore { enum Mode: Equatable { case select(isForOrigin: Bool) @@ -26,6 +26,8 @@ class NetworkSelectionStore: ObservableObject { /// The network the user wishes to choose for adding a custom asset @Published var networkSelectionInForm: BraveWallet.NetworkInfo? + var isObserving: Bool = false + init( mode: Mode = .select(isForOrigin: false), networkStore: NetworkStore diff --git a/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift b/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift index b58dd699446..e543375a6ae 100644 --- a/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift @@ -13,7 +13,7 @@ import Combine /// An interface that helps you interact with a json-rpc service /// /// This wraps a JsonRpcService that you would obtain through BraveCore and makes it observable -public class NetworkStore: ObservableObject { +public class NetworkStore: ObservableObject, WalletObserverStore { enum SetSelectedChainError: Error { case selectedChainHasNoAccounts @@ -53,8 +53,14 @@ public class NetworkStore: ObservableObject { private let walletService: BraveWalletBraveWalletService private let swapService: BraveWalletSwapService private let assetManager: WalletUserAssetManagerType + private var rpcServiceObserver: JsonRpcServiceObserver? + private var keyringServiceObserver: KeyringServiceObserver? private weak var networkSelectionStore: NetworkSelectionStore? + + var isObserving: Bool { + rpcServiceObserver != nil && keyringServiceObserver != nil + } public init( keyringService: BraveWalletKeyringService, @@ -70,8 +76,76 @@ public class NetworkStore: ObservableObject { self.swapService = swapService self.assetManager = userAssetManager self.origin = origin - rpcService.add(self) - keyringService.add(self) + + self.setupObservers() + } + + func tearDown() { + rpcServiceObserver = nil + keyringServiceObserver = nil + + networkSelectionStore?.tearDown() + } + + func setupObservers() { + guard !isObserving else { return } + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] chainId, coin, origin in + Task { @MainActor [self] in + // Verify correct account is selected for the new network. + // This could occur from Eth Switch Chain request when Solana account selected. + let accountId = await self?.walletService.ensureSelectedAccount(forChain: coin, chainId: chainId) + if accountId == nil { + assertionFailure("Should not have a nil selectedAccount for any network.") + } + // Sync our local properties with updated values + if let origin, origin == self?.origin { + self?.selectedChainIdForOrigin = chainId + } else if origin == nil { + self?.defaultSelectedChainId = chainId + self?.isSwapSupported = await self?.swapService.isSwapSupported(chainId) ?? false + if let origin = self?.origin { + // The default network may be used for this origin if no + // other network was assigned for this origin. If so, we + // need to make sure the `selectedChainIdForOrigin` is updated + // to reflect the correct network. + if let network = await self?.rpcService.network(coin, origin: origin) { + self?.selectedChainIdForOrigin = network.chainId + } + } + } + } + }, + _onAddEthereumChainRequestCompleted: { [weak self] _, _ in + Task { [self] in + await self?.updateChainList() + } + } + ) + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _selectedWalletAccountChanged: { [weak self] account in + Task { @MainActor [self] in + if self?.defaultSelectedChain.coin != account.coin { + if let selectedNetwork = await self?.rpcService.network(account.coin, origin: nil) { + self?.defaultSelectedChainId = selectedNetwork.chainId + } + } + if let origin = self?.origin, self?.selectedChainForOrigin.coin != account.coin { + // The default network may be used for this origin if no + // other network was assigned for this origin. If so, we + // need to make sure the `selectedChainIdForOrigin` is updated + // to reflect the correct network. + if let selectedNetwork = await self?.rpcService.network(account.coin, origin: origin) { + self?.selectedChainIdForOrigin = selectedNetwork.chainId + } + } + } + } + ) + + self.networkSelectionStore?.setupObservers() } @MainActor func setup() async { @@ -273,90 +347,6 @@ public class NetworkStore: ObservableObject { } } -extension NetworkStore: BraveWalletJsonRpcServiceObserver { - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - Task { - await updateChainList() - } - } - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - Task { @MainActor in - // Verify correct account is selected for the new network. - // This could occur from Eth Switch Chain request when Solana account selected. - let accountId = await walletService.ensureSelectedAccount(forChain: coin, chainId: chainId) - if accountId == nil { - assertionFailure("Should not have a nil selectedAccount for any network.") - } - // Sync our local properties with updated values - if let origin, origin == self.origin { - selectedChainIdForOrigin = chainId - } else if origin == nil { - defaultSelectedChainId = chainId - isSwapSupported = await swapService.isSwapSupported(chainId) - if let origin = self.origin { - // The default network may be used for this origin if no - // other network was assigned for this origin. If so, we - // need to make sure the `selectedChainIdForOrigin` is updated - // to reflect the correct network. - let network = await rpcService.network(coin, origin: origin) - selectedChainIdForOrigin = network.chainId - } - } - } - } -} - -extension NetworkStore: BraveWalletKeyringServiceObserver { - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - Task { @MainActor in - if defaultSelectedChain.coin != account.coin { - let selectedNetwork = await rpcService.network(account.coin, origin: nil) - defaultSelectedChainId = selectedNetwork.chainId - } - if let origin, selectedChainForOrigin.coin != account.coin { - // The default network may be used for this origin if no - // other network was assigned for this origin. If so, we - // need to make sure the `selectedChainIdForOrigin` is updated - // to reflect the correct network. - let selectedNetwork = await rpcService.network(account.coin, origin: origin) - selectedChainIdForOrigin = selectedNetwork.chainId - } - } - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringReset() { - } - - public func locked() { - } - - public func unlocked() { - } - - public func backedUp() { - } - - public func accountsChanged() { - } - - public func autoLockMinutesChanged() { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - extension Array where Element == BraveWallet.NetworkInfo { /// Returns the primary networks in Self. var primaryNetworks: [BraveWallet.NetworkInfo] { diff --git a/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift b/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift index 85a41f192d7..929533ffbc9 100644 --- a/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift @@ -187,7 +187,7 @@ struct BalanceTimePrice: DataPoint, Equatable { } /// A store containing data around the users assets -public class PortfolioStore: ObservableObject { +public class PortfolioStore: ObservableObject, WalletObserverStore { /// The dollar amount of your portfolio @Published private(set) var balance: String = "$0.00" /// The users visible fungible token groups. @@ -269,6 +269,7 @@ public class PortfolioStore: ObservableObject { rpcService: self.rpcService, keyringService: self.keyringService, assetRatioService: self.assetRatioService, + walletService: self.walletService, ipfsApi: self.ipfsApi, userAssetManager: self.assetManager ) @@ -291,6 +292,13 @@ public class PortfolioStore: ObservableObject { private let blockchainRegistry: BraveWalletBlockchainRegistry private let ipfsApi: IpfsAPI private let assetManager: WalletUserAssetManagerType + private var rpcServiceObserver: JsonRpcServiceObserver? + private var keyringServiceObserver: KeyringServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + rpcServiceObserver != nil && keyringServiceObserver != nil && walletServiceObserver != nil + } public init( keyringService: BraveWalletKeyringService, @@ -309,9 +317,7 @@ public class PortfolioStore: ObservableObject { self.ipfsApi = ipfsApi self.assetManager = userAssetManager - self.rpcService.add(self) - self.keyringService.add(self) - self.walletService.add(self) + self.setupObservers() keyringService.isLocked { [self] isLocked in if !isLocked { @@ -328,6 +334,59 @@ public class PortfolioStore: ObservableObject { Preferences.Wallet.nonSelectedNetworksFilter.observe(from: self) } + func tearDown() { + rpcServiceObserver = nil + keyringServiceObserver = nil + walletServiceObserver = nil + + userAssetsStore.tearDown() + } + + func setupObservers() { + guard !isObserving else { return } + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, _, _ in + self?.update() + } + ) + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _unlocked: { [weak self] in + DispatchQueue.main.async { + self?.update() + } + }, + _accountsChanged: { [weak self] in + Task { @MainActor [self] in + // An account was added or removed, `update()` will update `allAccounts`. + self?.update() + } + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onDefaultBaseCurrencyChanged: { [weak self] currency in + self?.currencyCode = currency + }, + _onNetworkListChanged: { [weak self] in + Task { @MainActor [self] in + // A network was added or removed, `update()` will update `allNetworks`. + self?.update() + } + }, + _onDiscoverAssetsStarted: { [weak self] in + self?.isLoadingDiscoverAssets = true + }, + _onDiscoverAssetsCompleted: { [weak self] discoveredAssets in + self?.isLoadingDiscoverAssets = false + // assets update will be called via `CryptoStore` + } + ) + + self.userAssetsStore.setupObservers() + } + func update() { self.updateTask?.cancel() self.updateTask = Task { @MainActor in @@ -632,93 +691,6 @@ public class PortfolioStore: ObservableObject { } } -extension PortfolioStore: BraveWalletJsonRpcServiceObserver { - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - update() - } -} - -extension PortfolioStore: BraveWalletKeyringServiceObserver { - public func keyringReset() { - } - - public func accountsChanged() { - Task { @MainActor in - // An account was added or removed, `update()` will update `allAccounts`. - update() - } - } - public func backedUp() { - } - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - public func locked() { - } - public func unlocked() { - DispatchQueue.main.async { [self] in - update() - } - } - public func autoLockMinutesChanged() { - } - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension PortfolioStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - currencyCode = currency - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - Task { @MainActor in - // A network was added or removed, `update()` will update `allNetworks`. - update() - } - } - - public func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDiscoverAssetsStarted() { - isLoadingDiscoverAssets = true - } - - public func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { - isLoadingDiscoverAssets = false - // assets update will be called via `CryptoStore` - } - - public func onResetWallet() { - } -} - extension PortfolioStore: PreferencesObserver { func saveFilters(_ filters: Filters) { isSavingFilters = true diff --git a/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift b/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift index ef92e8514cc..9d99e7acb23 100644 --- a/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift @@ -6,7 +6,7 @@ import SwiftUI import BraveCore -class SelectAccountTokenStore: ObservableObject { +class SelectAccountTokenStore: ObservableObject, WalletObserverStore { struct AccountSection: Equatable, Identifiable { struct TokenBalance: Equatable, Identifiable { @@ -112,6 +112,11 @@ class SelectAccountTokenStore: ObservableObject { private let assetRatioService: BraveWalletAssetRatioService private let ipfsApi: IpfsAPI private let assetManager: WalletUserAssetManagerType + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + walletServiceObserver != nil + } init( didSelect: @escaping (BraveWallet.AccountInfo, BraveWallet.BlockchainToken) -> Void, @@ -131,7 +136,32 @@ class SelectAccountTokenStore: ObservableObject { self.ipfsApi = ipfsApi self.assetManager = userAssetManager self.query = query ?? "" - walletService.add(self) + + self.setupObservers() + } + + func tearDown() { + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onDefaultBaseCurrencyChanged: { [weak self] currency in + self?.currencyCode = currency + }, + _onNetworkListChanged: { [weak self] in + Task { @MainActor [self] in + // A network was added or removed, update our network filters for the change. + guard let rpcService = self?.rpcService else { return } + self?.networkFilters = await rpcService.allNetworksForSupportedCoins().map { network in + let existingSelectionValue = self?.networkFilters.first(where: { $0.model.chainId == network.chainId})?.isSelected + return .init(isSelected: existingSelectionValue ?? true, model: network) + } + } + } + ) } func resetFilters() { @@ -315,36 +345,6 @@ extension SelectAccountTokenStore { } #endif -extension SelectAccountTokenStore: BraveWalletBraveWalletServiceObserver { - func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { } - - func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { } - - func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { } - - func onDefaultBaseCurrencyChanged(_ currency: String) { - currencyCode = currency - } - - func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { } - - func onNetworkListChanged() { - Task { @MainActor in - // A network was added or removed, update our network filters for the change. - self.networkFilters = await self.rpcService.allNetworksForSupportedCoins().map { network in - let existingSelectionValue = self.networkFilters.first(where: { $0.model.chainId == network.chainId})?.isSelected - return .init(isSelected: existingSelectionValue ?? true, model: network) - } - } - } - - func onDiscoverAssetsStarted() { } - - func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { } - - func onResetWallet() { } -} - extension Array where Element == SelectAccountTokenStore.AccountSection.TokenBalance { func filterNonZeroBalances(shouldFilter: Bool = true) -> Self { guard shouldFilter else { return self } diff --git a/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift b/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift index 15dead85534..8e98b6db513 100644 --- a/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift @@ -10,7 +10,7 @@ import BigNumber import Combine /// A store contains data for sending tokens -public class SendTokenStore: ObservableObject { +public class SendTokenStore: ObservableObject, WalletObserverStore { /// User's asset with selected account and chain @Published var userAssets: [BraveWallet.BlockchainToken] = [] /// The current selected token to send. Default with nil value. @@ -153,6 +153,12 @@ public class SendTokenStore: ObservableObject { private var metadataCache: [String: NFTMetadata] = [:] private let ipfsApi: IpfsAPI private let assetManager: WalletUserAssetManagerType + private var keyringServiceObserver: KeyringServiceObserver? + private var rpcServiceObserver: JsonRpcServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && rpcServiceObserver != nil + } public init( keyringService: BraveWalletKeyringService, @@ -179,8 +185,44 @@ public class SendTokenStore: ObservableObject { self.ipfsApi = ipfsApi self.assetManager = userAssetManager - self.keyringService.add(self) - self.rpcService.add(self) + self.setupObservers() + } + + func tearDown() { + keyringServiceObserver = nil + rpcServiceObserver = nil + + selectTokenStore.tearDown() + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _selectedWalletAccountChanged: { [weak self] _ in + guard let self else { return } + self.selectedSendTokenBalance = nil + self.addressError = nil + self.update() // `selectedSendTokenBalance` needs updated for new account + self.validateSendAddress() // `sendAddress` may equal selected account address + } + ) + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] _, coin, _ in + guard let self else { return } + self.selectedSendToken = nil + self.selectedSendTokenBalance = nil + if coin != .eth { // if changed to ethereum coin network, address is still valid + // offchain resolve is for ENS-only, for other coin types the address is invalid + self.isOffchainResolveRequired = false + } + self.update() // `selectedSendToken` & `selectedSendTokenBalance` need updated for new chain + self.validateSendAddress() // `sendAddress` may no longer be valid if coin type changed + } + ) + + self.selectTokenStore.setupObservers() } func suggestedAmountTapped(_ amount: ShortcutAmountGrid.Amount) { @@ -657,60 +699,4 @@ public class SendTokenStore: ObservableObject { } } -extension SendTokenStore: BraveWalletKeyringServiceObserver { - public func keyringReset() { - } - - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - public func locked() { - } - - public func unlocked() { - } - - public func backedUp() { - } - - public func accountsChanged() { - } - - public func autoLockMinutesChanged() { - } - - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - selectedSendTokenBalance = nil - addressError = nil - update() // `selectedSendTokenBalance` needs updated for new account - validateSendAddress() // `sendAddress` may equal selected account address - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension SendTokenStore: BraveWalletJsonRpcServiceObserver { - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - selectedSendToken = nil - selectedSendTokenBalance = nil - if coin != .eth { // if changed to ethereum coin network, address is still valid - // offchain resolve is for ENS-only, for other coin types the address is invalid - isOffchainResolveRequired = false - } - update() // `selectedSendToken` & `selectedSendTokenBalance` need updated for new chain - validateSendAddress() // `sendAddress` may no longer be valid if coin type changed - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } -} diff --git a/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift b/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift index e72736d8e4c..99a59408118 100644 --- a/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift @@ -10,7 +10,7 @@ import Data import Preferences import Combine -public class SettingsStore: ObservableObject { +public class SettingsStore: ObservableObject, WalletObserverStore { /// The number of minutes to wait until the Brave Wallet is automatically locked @Published var autoLockInterval: AutoLockInterval = .minute { didSet { @@ -80,6 +80,12 @@ public class SettingsStore: ObservableObject { private let txService: BraveWalletTxService let ipfsApi: IpfsAPI private let keychain: KeychainType + private var keyringServiceObserver: KeyringServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && walletServiceObserver != nil + } public init( keyringService: BraveWalletKeyringService, @@ -96,8 +102,30 @@ public class SettingsStore: ObservableObject { self.ipfsApi = ipfsApi self.keychain = keychain - keyringService.add(self) - walletService.add(self) + self.setupObservers() + } + + func tearDown() { + keyringServiceObserver = nil + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _autoLockMinutesChanged: { [weak self] in + self?.keyringService.autoLockMinutes { minutes in + self?.autoLockInterval = .init(value: minutes) + } + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onDefaultBaseCurrencyChanged: { [weak self] currency in + self?.currencyCode = CurrencyCode(code: currency) + } + ) } func setup() { @@ -166,76 +194,18 @@ public class SettingsStore: ObservableObject { } } -extension SettingsStore: BraveWalletKeyringServiceObserver { - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringReset() { - } - - public func locked() { - } - - public func unlocked() { - } - - public func backedUp() { - } - - public func accountsChanged() { - } - - public func autoLockMinutesChanged() { - keyringService.autoLockMinutes { [weak self] minutes in - self?.autoLockInterval = .init(value: minutes) - } - } - - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension SettingsStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - currencyCode = CurrencyCode(code: currency) - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - } - - public func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDiscoverAssetsStarted() { - } - - public func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { +#if DEBUG +// for testing +extension SettingsStore { + func autoLockMinutesChanged() { + keyringServiceObserver?.autoLockMinutesChanged() } - public func onResetWallet() { + func onDefaultBaseCurrencyChanged(_ currency: String) { + walletServiceObserver?.onDefaultBaseCurrencyChanged(currency) } } +#endif struct CurrencyCode: Hashable, Identifiable { let code: String diff --git a/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift b/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift index 51f3084a999..4f01193f5aa 100644 --- a/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift @@ -10,7 +10,7 @@ import Strings import Combine /// A store contains data for swap tokens -public class SwapTokenStore: ObservableObject { +public class SwapTokenStore: ObservableObject, WalletObserverStore { /// All tokens for searching use @Published var allTokens: [BraveWallet.BlockchainToken] = [] /// The current selected token to swap from. Default with nil value. @@ -129,6 +129,12 @@ public class SwapTokenStore: ObservableObject { private var prefilledToken: BraveWallet.BlockchainToken? /// The JupiterQuote currently being displayed for Solana swap. The quote needs preserved to create the swap transaction. private var jupiterQuote: BraveWallet.JupiterQuote? + private var keyringServiceObserver: KeyringServiceObserver? + private var rpcServiceObserver: JsonRpcServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && rpcServiceObserver != nil + } enum SwapParamsBase { // calculating based on sell asset amount @@ -173,8 +179,57 @@ public class SwapTokenStore: ObservableObject { self.assetManager = userAssetManager self.prefilledToken = prefilledToken - self.keyringService.add(self) - self.rpcService.add(self) + self.setupObservers() + } + + func tearDown() { + keyringServiceObserver = nil + rpcServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _selectedWalletAccountChanged: { [weak self] account in + Task { @MainActor [self] in + guard let network = await self?.rpcService.network(account.coin, origin: nil), + let isSwapSupported = await self?.swapService.isSwapSupported(network.chainId), + isSwapSupported + else { + self?.accountInfo = account + return + } + + self?.selectedFromToken = nil + self?.selectedToToken = nil + self?.prepare(with: account) { + self?.fetchPriceQuote(base: .perSellAsset) + } + } + } + ) + self.rpcServiceObserver = JsonRpcServiceObserver( + rpcService: rpcService, + _chainChangedEvent: { [weak self] chainId, coin, origin in + Task { @MainActor [self] in + guard let isSwapSupported = await self?.swapService.isSwapSupported(chainId), + isSwapSupported + else { return } + guard let _ = await self?.walletService.ensureSelectedAccount(forChain: coin, chainId: chainId), + let selectedAccount = await self?.keyringService.allAccounts().selectedAccount else { + assertionFailure("selectedAccount should never be nil.") + return + } + + self?.selectedFromToken = nil + self?.selectedToToken = nil + self?.prepare(with: selectedAccount) { + self?.fetchPriceQuote(base: .perSellAsset) + } + } + } + ) } private func fetchTokenBalance( @@ -893,77 +948,3 @@ public class SwapTokenStore: ObservableObject { } #endif } - -extension SwapTokenStore: BraveWalletKeyringServiceObserver { - public func keyringReset() { - } - - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - public func locked() { - } - - public func unlocked() { - } - - public func backedUp() { - } - - public func accountsChanged() { - } - - public func autoLockMinutesChanged() { - } - - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - Task { @MainActor in - let network = await rpcService.network(account.coin, origin: nil) - let isSwapSupported = await swapService.isSwapSupported(network.chainId) - guard isSwapSupported else { - accountInfo = account - return - } - - selectedFromToken = nil - selectedToToken = nil - prepare(with: account) { [self] in - fetchPriceQuote(base: .perSellAsset) - } - } - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension SwapTokenStore: BraveWalletJsonRpcServiceObserver { - public func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { - Task { @MainActor in - let isSwapSupported = await swapService.isSwapSupported(chainId) - guard isSwapSupported else { return } - guard let _ = await walletService.ensureSelectedAccount(forChain: coin, chainId: chainId), - let selectedAccount = await keyringService.allAccounts().selectedAccount else { - assertionFailure("selectedAccount should never be nil.") - return - } - selectedFromToken = nil - selectedToToken = nil - prepare(with: selectedAccount) { [self] in - fetchPriceQuote(base: .perSellAsset) - } - } - } - - public func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { - } - - public func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { - } -} diff --git a/Sources/BraveWallet/Crypto/Stores/TabDappStore.swift b/Sources/BraveWallet/Crypto/Stores/TabDappStore.swift index 79967767690..8763916b70e 100644 --- a/Sources/BraveWallet/Crypto/Stores/TabDappStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TabDappStore.swift @@ -7,7 +7,7 @@ import Foundation import Combine /// This Store will be created for each Tab -public class TabDappStore: ObservableObject { +public class TabDappStore: ObservableObject, WalletObserverStore { /// A set of solana account addresses that are currently connected to the dapp @Published public var solConnectedAddresses: Set = .init() /// The latest pending dapp permission request. A permission request will be created right after @@ -15,5 +15,7 @@ public class TabDappStore: ObservableObject { /// the change of this value. @Published public var latestPendingPermissionRequest: WebpagePermissionRequest? + var isObserving: Bool = false + public init() {} } diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift index f891eb4a27b..2bade073e68 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift @@ -9,7 +9,7 @@ import BigNumber import Strings import Combine -public class TransactionConfirmationStore: ObservableObject { +public class TransactionConfirmationStore: ObservableObject, WalletObserverStore { /// The value that are being sent/swapped/approved in this transaction @Published var value: String = "" /// The symbol of token that are being send/swapped/approved in this transaction @@ -140,6 +140,12 @@ public class TransactionConfirmationStore: ObservableObject { private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy private let assetManager: WalletUserAssetManagerType private var selectedChain: BraveWallet.NetworkInfo = .init() + private var txServiceObserver: TxServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + txServiceObserver != nil && walletServiceObserver != nil + } init( assetRatioService: BraveWalletAssetRatioService, @@ -162,14 +168,77 @@ public class TransactionConfirmationStore: ObservableObject { self.solTxManagerProxy = solTxManagerProxy self.assetManager = userAssetManager - self.txService.add(self) - self.walletService.add(self) + self.setupObservers() walletService.defaultBaseCurrency { [self] currencyCode in self.currencyCode = currencyCode } } + func tearDown() { + txServiceObserver = nil + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.txServiceObserver = TxServiceObserver( + txService: txService, + _onNewUnapprovedTx: { _ in + // won't have any new unapproved tx being added if you on tx confirmation panel + }, + _onUnapprovedTxUpdated: { [weak self] txInfo in + Task { @MainActor [self] in + // refresh the unapproved transaction list, as well as tx details UI + // first update `allTxs` with the new updated txInfo(txStatus) + if let index = self?.allTxs.firstIndex(where: { $0.id == txInfo.id }) { + self?.allTxs[index] = txInfo + } + + // update details UI if the current active tx is updated + if self?.activeTransactionId == txInfo.id { + self?.updateTransaction(with: txInfo) + self?.activeTxStatus = txInfo.txStatus + } + + // if somehow the current active transaction no longer exists + // set the first `.unapproved` tx as the new `activeTransactionId` + if let unapprovedTxs = self?.unapprovedTxs, !unapprovedTxs.contains(where: { $0.id == self?.activeTransactionId }) { + self?.activeTransactionId = self?.unapprovedTxs.first?.id ?? "" + } + } + }, + _onTransactionStatusChanged: { [weak self] txInfo in + Task { @MainActor [self] in + // once we come here. it means user either rejects or confirms a transaction + + // first update `allTxs` with the new updated txInfo(txStatus) + if let index = self?.allTxs.firstIndex(where: { $0.id == txInfo.id }) { + self?.allTxs[index] = txInfo + } + + // only update the `activeTransactionId` if the current active transaction status + // becomes `.rejected`/`.dropped` + if self?.activeTransactionId == txInfo.id, txInfo.txStatus == .rejected || txInfo.txStatus == .dropped { + let indexOfChangedTx = self?.unapprovedTxs.firstIndex(where: { $0.id == txInfo.id }) ?? 0 + let newIndex = indexOfChangedTx > 0 ? indexOfChangedTx - 1 : 0 + self?.activeTransactionId = self?.unapprovedTxs[safe: newIndex]?.id ?? self?.unapprovedTxs.first?.id ?? "" + } else { + if self?.activeTransactionId == txInfo.id { + self?.activeTxStatus = txInfo.txStatus + } + } + } + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onDefaultBaseCurrencyChanged: { [weak self] currency in + self?.currencyCode = currency + } + ) + } + func nextTransaction() { if let index = unapprovedTxs.firstIndex(where: { $0.id == activeTransactionId }) { var nextIndex = unapprovedTxs.index(after: index) @@ -727,88 +796,3 @@ struct TransactionProviderError { let code: Int let message: String } - -extension TransactionConfirmationStore: BraveWalletTxServiceObserver { - public func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) { - // won't have any new unapproved tx being added if you on tx confirmation panel - } - public func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) { - Task { @MainActor in - // once we come here. it means user either rejects or confirms a transaction - - // first update `allTxs` with the new updated txInfo(txStatus) - if let index = allTxs.firstIndex(where: { $0.id == txInfo.id }) { - allTxs[index] = txInfo - } - - // only update the `activeTransactionId` if the current active transaction status - // becomes `.rejected`/`.dropped` - if activeTransactionId == txInfo.id, txInfo.txStatus == .rejected || txInfo.txStatus == .dropped { - let indexOfChangedTx = unapprovedTxs.firstIndex(where: { $0.id == txInfo.id }) ?? 0 - let newIndex = indexOfChangedTx > 0 ? indexOfChangedTx - 1 : 0 - activeTransactionId = unapprovedTxs[safe: newIndex]?.id ?? unapprovedTxs.first?.id ?? "" - } else { - if activeTransactionId == txInfo.id { - activeTxStatus = txInfo.txStatus - } - } - } - } - public func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) { - Task { @MainActor in - // refresh the unapproved transaction list, as well as tx details UI - // first update `allTxs` with the new updated txInfo(txStatus) - if let index = allTxs.firstIndex(where: { $0.id == txInfo.id }) { - allTxs[index] = txInfo - } - - // update details UI if the current active tx is updated - if activeTransactionId == txInfo.id { - updateTransaction(with: txInfo) - activeTxStatus = txInfo.txStatus - } - - // if somehow the current active transaction no longer exists - // set the first `.unapproved` tx as the new `activeTransactionId` - if !unapprovedTxs.contains(where: { $0.id == activeTransactionId }) { - activeTransactionId = unapprovedTxs.first?.id ?? "" - } - } - } - - public func onTxServiceReset() { - } -} - -extension TransactionConfirmationStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { - } - - public func onDefaultWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { - currencyCode = currency - } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { - } - - public func onNetworkListChanged() { - } - - public func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { - } - - public func onDiscoverAssetsStarted() { - } - - public func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { - } - - public func onResetWallet() { - } -} diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift index 7dda58072ae..3b9d1047ca5 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift @@ -6,7 +6,7 @@ import SwiftUI import BraveCore -class TransactionDetailsStore: ObservableObject { +class TransactionDetailsStore: ObservableObject, WalletObserverStore { let transaction: BraveWallet.TransactionInfo @Published private(set) var parsedTransaction: ParsedTransaction? @@ -40,6 +40,8 @@ class TransactionDetailsStore: ObservableObject { /// This could occur with a dapp creating a transaction. private var tokenInfoCache: [String: BraveWallet.BlockchainToken] = [:] + var isObserving: Bool = false + init( transaction: BraveWallet.TransactionInfo, keyringService: BraveWalletKeyringService, diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift index ef76d186889..fee48ce5da4 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift @@ -6,7 +6,7 @@ import BraveCore import SwiftUI -class TransactionsActivityStore: ObservableObject { +class TransactionsActivityStore: ObservableObject, WalletObserverStore { @Published var transactionSummaries: [TransactionSummary] = [] @Published private(set) var currencyCode: String = CurrencyCode.usd.code { @@ -36,6 +36,13 @@ class TransactionsActivityStore: ObservableObject { private let txService: BraveWalletTxService private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy private let assetManager: WalletUserAssetManagerType + private var keyringServiceObserver: KeyringServiceObserver? + private var txServiceObserver: TxServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && txServiceObserver != nil && walletServiceObserver != nil + } init( keyringService: BraveWalletKeyringService, @@ -56,15 +63,60 @@ class TransactionsActivityStore: ObservableObject { self.solTxManagerProxy = solTxManagerProxy self.assetManager = userAssetManager - keyringService.add(self) - txService.add(self) - walletService.add(self) + self.setupObservers() Task { @MainActor in self.currencyCode = await walletService.defaultBaseCurrency() } } + func tearDown() { + keyringServiceObserver = nil + txServiceObserver = nil + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _accountsChanged: { [weak self] in + self?.update() + }, + _accountsAdded: { [weak self] _ in + self?.update() + } + ) + self.txServiceObserver = TxServiceObserver( + txService: txService, + _onNewUnapprovedTx: { [weak self] _ in + self?.update() + }, + _onUnapprovedTxUpdated: { [weak self] _ in + self?.update() + }, + _onTransactionStatusChanged: { [weak self] _ in + self?.update() + }, + _onTxServiceReset: { [weak self] in + self?.update() + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onNetworkListChanged: { [weak self] in + Task { @MainActor [self] in + // A network was added or removed, update our network filters for the change. + guard let rpcService = self?.rpcService else { return } + self?.networkFilters = await rpcService.allNetworksForSupportedCoins().map { network in + let existingSelectionValue = self?.networkFilters.first(where: { $0.model.chainId == network.chainId})?.isSelected + return .init(isSelected: existingSelectionValue ?? true, model: network) + } + } + } + ) + } + private var updateTask: Task? func update() { updateTask?.cancel() @@ -184,77 +236,3 @@ class TransactionsActivityStore: ObservableObject { ) } } - -extension TransactionsActivityStore: BraveWalletKeyringServiceObserver { - func keyringCreated(_ keyringId: BraveWallet.KeyringId) { } - - func keyringRestored(_ keyringId: BraveWallet.KeyringId) { } - - func keyringReset() { } - - func locked() { } - - func unlocked() { } - - func backedUp() { } - - func accountsChanged() { - update() - } - - func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - update() - } - - func autoLockMinutesChanged() { } - - func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { } - - func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { } -} - -extension TransactionsActivityStore: BraveWalletTxServiceObserver { - func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) { - update() - } - - func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) { - update() - } - - func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) { - update() - } - - func onTxServiceReset() { - update() - } -} - -extension TransactionsActivityStore: BraveWalletBraveWalletServiceObserver { - func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { } - - func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { } - - func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { } - - func onDefaultBaseCurrencyChanged(_ currency: String) { } - - func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { } - - func onNetworkListChanged() { - Task { @MainActor in - // A network was added or removed, update our network filters for the change. - self.networkFilters = await self.rpcService.allNetworksForSupportedCoins().map { network in - let existingSelectionValue = self.networkFilters.first(where: { $0.model.chainId == network.chainId})?.isSelected - return .init(isSelected: existingSelectionValue ?? true, model: network) - } - } - } - - func onDiscoverAssetsStarted() { } - - func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { } - - func onResetWallet() { } -} diff --git a/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift b/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift index 8cefa3e0a9b..96d80d7c9c1 100644 --- a/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift @@ -9,7 +9,7 @@ import Combine import Data import Preferences -public class AssetStore: ObservableObject, Equatable { +public class AssetStore: ObservableObject, Equatable, WalletObserverStore { @Published var token: BraveWallet.BlockchainToken @Published var isVisible: Bool { didSet { @@ -22,6 +22,8 @@ public class AssetStore: ObservableObject, Equatable { private let ipfsApi: IpfsAPI private let assetManager: WalletUserAssetManagerType private(set) var isCustomToken: Bool + + var isObserving: Bool = false init( rpcService: BraveWalletJsonRpcService, @@ -50,7 +52,7 @@ public class AssetStore: ObservableObject, Equatable { } } -public class UserAssetsStore: ObservableObject { +public class UserAssetsStore: ObservableObject, WalletObserverStore { @Published private(set) var assetStores: [AssetStore] = [] @Published var isSearchingToken: Bool = false @Published var networkFilters: [Selectable] = [] { @@ -64,16 +66,24 @@ public class UserAssetsStore: ObservableObject { private let rpcService: BraveWalletJsonRpcService private let keyringService: BraveWalletKeyringService private let assetRatioService: BraveWalletAssetRatioService + private let walletService: BraveWalletBraveWalletService private let ipfsApi: IpfsAPI private let assetManager: WalletUserAssetManagerType private var allTokens: [BraveWallet.BlockchainToken] = [] private var timer: Timer? + private var keyringServiceObserver: KeyringServiceObserver? + private var walletServiceObserver: WalletServiceObserver? + + var isObserving: Bool { + keyringServiceObserver != nil && walletServiceObserver != nil + } public init( blockchainRegistry: BraveWalletBlockchainRegistry, rpcService: BraveWalletJsonRpcService, keyringService: BraveWalletKeyringService, assetRatioService: BraveWalletAssetRatioService, + walletService: BraveWalletBraveWalletService, ipfsApi: IpfsAPI, userAssetManager: WalletUserAssetManagerType ) { @@ -81,13 +91,43 @@ public class UserAssetsStore: ObservableObject { self.rpcService = rpcService self.keyringService = keyringService self.assetRatioService = assetRatioService + self.walletService = walletService self.ipfsApi = ipfsApi self.assetManager = userAssetManager - self.keyringService.add(self) + + self.setupObservers() Preferences.Wallet.showTestNetworks.observe(from: self) } + func tearDown() { + keyringServiceObserver = nil + walletServiceObserver = nil + } + + func setupObservers() { + guard !isObserving else { return } + self.keyringServiceObserver = KeyringServiceObserver( + keyringService: keyringService, + _keyringCreated: { [weak self] _ in + self?.update() + } + ) + self.walletServiceObserver = WalletServiceObserver( + walletService: walletService, + _onNetworkListChanged: { [weak self] in + Task { @MainActor [self] in + // A network was added or removed, update our network filters for the change. + guard let rpcService = self?.rpcService else { return } + self?.networkFilters = await rpcService.allNetworksForSupportedCoins().map { network in + let existingSelectionValue = self?.networkFilters.first(where: { $0.model.chainId == network.chainId})?.isSelected + return .init(isSelected: existingSelectionValue ?? true, model: network) + } + } + } + ) + } + func update() { Task { @MainActor in // setup network filters if not currently setup @@ -234,70 +274,6 @@ public class UserAssetsStore: ObservableObject { } } -extension UserAssetsStore: BraveWalletKeyringServiceObserver { - public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - update() - } - - public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { - } - - public func keyringReset() { - } - - public func locked() { - } - - public func unlocked() { - } - - public func backedUp() { - } - - public func accountsChanged() { - } - - public func autoLockMinutesChanged() { - } - - public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { - } - - public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { - } - - public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { - } -} - -extension UserAssetsStore: BraveWalletBraveWalletServiceObserver { - public func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { } - - public func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { } - - public func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { } - - public func onDefaultBaseCurrencyChanged(_ currency: String) { } - - public func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { } - - public func onNetworkListChanged() { - Task { @MainActor in - // A network was added or removed, update our network filters for the change. - self.networkFilters = await self.rpcService.allNetworksForSupportedCoins().map { network in - let existingSelectionValue = self.networkFilters.first(where: { $0.model.chainId == network.chainId})?.isSelected - return .init(isSelected: existingSelectionValue ?? true, model: network) - } - } - } - - public func onDiscoverAssetsStarted() { } - - public func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { } - - public func onResetWallet() { } -} - extension UserAssetsStore: PreferencesObserver { public func preferencesDidChange(for key: String) { if key == Preferences.Wallet.showTestNetworks.key { diff --git a/Sources/BraveWallet/Crypto/Stores/WalletStore.swift b/Sources/BraveWallet/Crypto/Stores/WalletStore.swift index d97fc0cae68..4bb2aa0baea 100644 --- a/Sources/BraveWallet/Crypto/Stores/WalletStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/WalletStore.swift @@ -23,6 +23,39 @@ public class WalletStore { } public let onPendingRequestUpdated = PassthroughSubject() + + var isPresentingWalletPanel: Bool = false { + didSet { + if oldValue, !isPresentingWalletPanel { // dismiss + if !isPresentingFullWallet { // both full wallet and wallet panel are dismissed + self.tearDown() + } else { + // dismiss panel to present full screen. observer should be setup") + self.setupObservers() + } + } else if !oldValue, isPresentingWalletPanel { // present + self.setupObservers() + } + } + } + var isPresentingFullWallet: Bool = false { + didSet { + if oldValue, !isPresentingFullWallet { // dismiss + if !isPresentingWalletPanel { // both panel and full wallet are dismissed + self.tearDown() + } else { + // panel is still visible, do not tear down + } + } else if !oldValue, isPresentingFullWallet { // present + if isPresentingWalletPanel { + // observers should be setup when wallet panel is presented + } else { + // either open from browser settings or from wallet panel + self.setupObservers() + } + } + } + } // MARK: - @@ -55,6 +88,16 @@ public class WalletStore { ipfsApi: ipfsApi ) } + + public func setupObservers() { + keyringStore.setupObservers() + cryptoStore?.setupObservers() + } + + public func tearDown() { + keyringStore.tearDown() + cryptoStore?.tearDown() + } private func setUp( keyringService: BraveWalletKeyringService, @@ -74,6 +117,9 @@ public class WalletStore { .sink { [weak self] isDefaultKeyringCreated in guard let self = self else { return } if !isDefaultKeyringCreated, self.cryptoStore != nil { + // only tear down `CryptoStore` since we still need to listen + // default keyring creation if user didn't dismiss the wallet after reset + self.cryptoStore?.tearDown() self.cryptoStore = nil } else if isDefaultKeyringCreated, self.cryptoStore == nil { self.cryptoStore = CryptoStore( @@ -104,3 +150,16 @@ public class WalletStore { } } } + +protocol WalletObserverStore: AnyObject { + var isObserving: Bool { get } + func tearDown() + func setupObservers() +} + +extension WalletObserverStore { + func tearDown() { + } + func setupObservers() { + } +} diff --git a/Sources/BraveWallet/Crypto/WalletServiceObservers.swift b/Sources/BraveWallet/Crypto/WalletServiceObservers.swift new file mode 100644 index 00000000000..f687821cf12 --- /dev/null +++ b/Sources/BraveWallet/Crypto/WalletServiceObservers.swift @@ -0,0 +1,223 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import BraveCore + +class KeyringServiceObserver: BraveWalletKeyringServiceObserver { + + var _keyringReset: (() -> Void)? + var _keyringCreated: ((_ keyringId: BraveWallet.KeyringId) -> Void)? + var _keyringRestored: ((_ keyringId: BraveWallet.KeyringId) -> Void)? + var _locked: (() -> Void)? + var _unlocked: (() -> Void)? + var _backedUp: (() -> Void)? + var _accountsChanged: (() -> Void)? + var _autoLockMinutesChanged: (() -> Void)? + var _selectedWalletAccountChanged: ((_ account: BraveWallet.AccountInfo) -> Void)? + var _selectedDappAccountChanged: ((_ coin: BraveWallet.CoinType, _ account: BraveWallet.AccountInfo?) -> Void)? + var _accountsAdded: ((_ addedAccounts: [BraveWallet.AccountInfo]) -> Void)? + + init( + keyringService: BraveWalletKeyringService, + _keyringReset: (() -> Void)? = nil, + _keyringCreated: ((BraveWallet.KeyringId) -> Void)? = nil, + _keyringRestored: ((BraveWallet.KeyringId) -> Void)? = nil, + _locked: (() -> Void)? = nil, + _unlocked: (() -> Void)? = nil, + _backedUp: (() -> Void)? = nil, + _accountsChanged: (() -> Void)? = nil, + _autoLockMinutesChanged: (() -> Void)? = nil, + _selectedWalletAccountChanged: ((BraveWallet.AccountInfo) -> Void)? = nil, + _selectedDappAccountChanged: ((BraveWallet.CoinType, BraveWallet.AccountInfo?) -> Void)? = nil, + _accountsAdded: (([BraveWallet.AccountInfo]) -> Void)? = nil + ) { + self._keyringReset = _keyringReset + self._keyringCreated = _keyringCreated + self._keyringRestored = _keyringRestored + self._locked = _locked + self._unlocked = _unlocked + self._backedUp = _backedUp + self._accountsChanged = _accountsChanged + self._autoLockMinutesChanged = _autoLockMinutesChanged + self._selectedWalletAccountChanged = _selectedWalletAccountChanged + self._selectedDappAccountChanged = _selectedDappAccountChanged + self._accountsAdded = _accountsAdded + keyringService.add(self) + } + + public func keyringReset() { + _keyringReset?() + } + public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { + _keyringCreated?(keyringId) + } + public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { + _keyringRestored?(keyringId) + } + public func locked() { + _locked?() + } + public func unlocked() { + _unlocked?() + } + public func backedUp() { + _backedUp?() + } + public func accountsChanged() { + _accountsChanged?() + } + public func autoLockMinutesChanged() { + _autoLockMinutesChanged?() + } + public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) { + _selectedWalletAccountChanged?(account) + } + public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) { + _selectedDappAccountChanged?(coin, account) + } + public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) { + _accountsAdded?(addedAccounts) + } +} + +class WalletServiceObserver: BraveWalletBraveWalletServiceObserver { + + var _onActiveOriginChanged: ((_ originInfo: BraveWallet.OriginInfo) -> Void)? + var _onDefaultEthereumWalletChanged: ((_ wallet: BraveWallet.DefaultWallet) -> Void)? + var _onDefaultSolanaWalletChanged: ((_ wallet: BraveWallet.DefaultWallet) -> Void)? + var _onDefaultBaseCurrencyChanged: ((_ currency: String) -> Void)? + var _onDefaultBaseCryptocurrencyChanged: ((_ cryptocurrency: String) -> Void)? + var _onNetworkListChanged: (() -> Void)? + var _onDiscoverAssetsStarted: (() -> Void)? + var _onDiscoverAssetsCompleted: ((_ discoveredAssets: [BraveWallet.BlockchainToken]) -> Void)? + var _onResetWallet: (() -> Void)? + + init( + walletService: BraveWalletBraveWalletService, + _onActiveOriginChanged: ((_ originInfo: BraveWallet.OriginInfo) -> Void)? = nil, + _onDefaultEthereumWalletChanged: ((_ wallet: BraveWallet.DefaultWallet) -> Void)? = nil, + _onDefaultSolanaWalletChanged: ((_ wallet: BraveWallet.DefaultWallet) -> Void)? = nil, + _onDefaultBaseCurrencyChanged: ((_ currency: String) -> Void)? = nil, + _onDefaultBaseCryptocurrencyChanged: ((_ cryptocurrency: String) -> Void)? = nil, + _onNetworkListChanged: (() -> Void)? = nil, + _onDiscoverAssetsStarted: (() -> Void)? = nil, + _onDiscoverAssetsCompleted: ((_ discoveredAssets: [BraveWallet.BlockchainToken]) -> Void)? = nil, + _onResetWallet: (() -> Void)? = nil + ) { + self._onActiveOriginChanged = _onActiveOriginChanged + self._onDefaultEthereumWalletChanged = _onDefaultEthereumWalletChanged + self._onDefaultSolanaWalletChanged = _onDefaultSolanaWalletChanged + self._onDefaultBaseCurrencyChanged = _onDefaultBaseCurrencyChanged + self._onDefaultBaseCryptocurrencyChanged = _onDefaultBaseCryptocurrencyChanged + self._onNetworkListChanged = _onNetworkListChanged + self._onDiscoverAssetsStarted = _onDiscoverAssetsStarted + self._onDiscoverAssetsCompleted = _onDiscoverAssetsCompleted + self._onResetWallet = _onResetWallet + walletService.add(self) + } + + func onActiveOriginChanged(_ originInfo: BraveWallet.OriginInfo) { + _onActiveOriginChanged?(originInfo) + } + + func onDefaultEthereumWalletChanged(_ wallet: BraveWallet.DefaultWallet) { + _onDefaultEthereumWalletChanged?(wallet) + } + + func onDefaultSolanaWalletChanged(_ wallet: BraveWallet.DefaultWallet) { + _onDefaultSolanaWalletChanged?(wallet) + } + + func onDefaultBaseCurrencyChanged(_ currency: String) { + _onDefaultBaseCurrencyChanged?(currency) + } + + func onDefaultBaseCryptocurrencyChanged(_ cryptocurrency: String) { + _onDefaultBaseCryptocurrencyChanged?(cryptocurrency) + } + + func onNetworkListChanged() { + _onNetworkListChanged?() + } + + func onDiscoverAssetsStarted() { + _onDiscoverAssetsStarted?() + } + + func onDiscoverAssetsCompleted(_ discoveredAssets: [BraveWallet.BlockchainToken]) { + _onDiscoverAssetsCompleted?(discoveredAssets) + } + + func onResetWallet() { + _onResetWallet?() + } +} + +class TxServiceObserver: BraveWalletTxServiceObserver { + var _onNewUnapprovedTx: ((_ txInfo: BraveWallet.TransactionInfo) -> Void)? + var _onUnapprovedTxUpdated: ((_ txInfo: BraveWallet.TransactionInfo) -> Void)? + var _onTransactionStatusChanged: ((_ txInfo: BraveWallet.TransactionInfo) -> Void)? + var _onTxServiceReset: (() -> Void)? + + init( + txService: BraveWalletTxService, + _onNewUnapprovedTx: ((_: BraveWallet.TransactionInfo) -> Void)? = nil, + _onUnapprovedTxUpdated: ((_: BraveWallet.TransactionInfo) -> Void)? = nil, + _onTransactionStatusChanged: ((_: BraveWallet.TransactionInfo) -> Void)? = nil, + _onTxServiceReset: (() -> Void)? = nil + ) { + self._onNewUnapprovedTx = _onNewUnapprovedTx + self._onUnapprovedTxUpdated = _onUnapprovedTxUpdated + self._onTransactionStatusChanged = _onTransactionStatusChanged + self._onTxServiceReset = _onTxServiceReset + txService.add(self) + } + + func onNewUnapprovedTx(_ txInfo: BraveWallet.TransactionInfo) { + _onNewUnapprovedTx?(txInfo) + } + + func onUnapprovedTxUpdated(_ txInfo: BraveWallet.TransactionInfo) { + _onUnapprovedTxUpdated?(txInfo) + } + + func onTransactionStatusChanged(_ txInfo: BraveWallet.TransactionInfo) { + _onTransactionStatusChanged?(txInfo) + } + + func onTxServiceReset() { + _onTxServiceReset?() + } +} + +class JsonRpcServiceObserver: BraveWalletJsonRpcServiceObserver { + var _chainChangedEvent: ((_ chainId: String, _ coin: BraveWallet.CoinType, _ origin: URLOrigin?) -> Void)? + var _onAddEthereumChainRequestCompleted: ((_ chainId: String, _ error: String) -> Void)? + var _onIsEip1559Changed: ((_ chainId: String, _ isEip1559: Bool) -> Void)? + + init( + rpcService: BraveWalletJsonRpcService, + _chainChangedEvent: ((_ chainId: String, _ coin: BraveWallet.CoinType, _ origin: URLOrigin?) -> Void)? = nil, + _onAddEthereumChainRequestCompleted: ((_ chainId: String, _ error: String) -> Void)? = nil, + _onIsEip1559Changed: ((_ chainId: String, _ isEip1559: Bool) -> Void)? = nil + ) { + self._chainChangedEvent = _chainChangedEvent + self._onAddEthereumChainRequestCompleted = _onAddEthereumChainRequestCompleted + self._onIsEip1559Changed = _onIsEip1559Changed + rpcService.add(self) + } + + func chainChangedEvent(_ chainId: String, coin: BraveWallet.CoinType, origin: URLOrigin?) { + _chainChangedEvent?(chainId, coin, origin) + } + + func onAddEthereumChainRequestCompleted(_ chainId: String, error: String) { + _onAddEthereumChainRequestCompleted?(chainId, error) + } + + func onIsEip1559Changed(_ chainId: String, isEip1559: Bool) { + _onIsEip1559Changed?(chainId, isEip1559) + } +} diff --git a/Sources/BraveWallet/Panels/RequestContainerView.swift b/Sources/BraveWallet/Panels/RequestContainerView.swift index d2f79da5885..80a91285882 100644 --- a/Sources/BraveWallet/Panels/RequestContainerView.swift +++ b/Sources/BraveWallet/Panels/RequestContainerView.swift @@ -28,6 +28,9 @@ struct RequestContainerView: View { keyringStore: keyringStore, onDismiss: onDismiss ) + .onDisappear { + cryptoStore.closeConfirmationStore() + } case .addSuggestedToken(let request): AddSuggestedTokenView( token: request.token, diff --git a/Sources/BraveWallet/Preview Content/MockStores.swift b/Sources/BraveWallet/Preview Content/MockStores.swift index fba420de024..4f87ffe86c8 100644 --- a/Sources/BraveWallet/Preview Content/MockStores.swift +++ b/Sources/BraveWallet/Preview Content/MockStores.swift @@ -163,6 +163,7 @@ extension UserAssetsStore { rpcService: MockJsonRpcService(), keyringService: MockKeyringService(), assetRatioService: MockAssetRatioService(), + walletService: MockBraveWalletService(), ipfsApi: TestIpfsAPI(), userAssetManager: TestableWalletUserAssetManager() ) diff --git a/Sources/BraveWallet/WalletHostingViewController.swift b/Sources/BraveWallet/WalletHostingViewController.swift index 3401787f922..768bc8cd6f2 100644 --- a/Sources/BraveWallet/WalletHostingViewController.swift +++ b/Sources/BraveWallet/WalletHostingViewController.swift @@ -52,6 +52,7 @@ public enum PresentingContext { public class WalletHostingViewController: UIHostingController { public weak var delegate: BraveWalletDelegate? private var cancellable: AnyCancellable? + private var walletStore: WalletStore? public init( walletStore: WalletStore, @@ -98,6 +99,7 @@ public class WalletHostingViewController: UIHostingController { self.dismiss(animated: true) } } + self.walletStore = walletStore } @available(*, unavailable) @@ -107,6 +109,7 @@ public class WalletHostingViewController: UIHostingController { deinit { gesture.view?.removeGestureRecognizer(gesture) + walletStore?.isPresentingFullWallet = false } private let gesture: WalletInteractionGestureRecognizer @@ -117,6 +120,11 @@ public class WalletHostingViewController: UIHostingController { UIDevice.current.forcePortraitIfIphone(for: UIApplication.shared) } + public override func viewDidLoad() { + super.viewDidLoad() + walletStore?.isPresentingFullWallet = true + } + // MARK: - public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { diff --git a/Sources/BraveWallet/WalletPanelHostingController.swift b/Sources/BraveWallet/WalletPanelHostingController.swift index e2f41129731..7b54a19658b 100644 --- a/Sources/BraveWallet/WalletPanelHostingController.swift +++ b/Sources/BraveWallet/WalletPanelHostingController.swift @@ -16,6 +16,7 @@ public class WalletPanelHostingController: UIHostingController (BraveWallet.TestKeyringService, BraveWallet.TestJsonRpcService, BraveWallet.TestBlockchainRegistry, BraveWallet.TestAssetRatioService) { + private func setupServices() -> (BraveWallet.TestKeyringService, BraveWallet.TestJsonRpcService, BraveWallet.TestBlockchainRegistry, BraveWallet.TestAssetRatioService, BraveWallet.TestBraveWalletService) { let keyringService = BraveWallet.TestKeyringService() keyringService._addObserver = { _ in } @@ -43,12 +43,15 @@ class UserAssetsStoreTests: XCTestCase { } let assetRatioService = BraveWallet.TestAssetRatioService() + + let walletService = BraveWallet.TestBraveWalletService() + walletService._addObserver = { _ in } - return (keyringService, rpcService, blockchainRegistry, assetRatioService) + return (keyringService, rpcService, blockchainRegistry, assetRatioService, walletService) } func testUpdate() { - let (keyringService, rpcService, blockchainRegistry, assetRatioService) = setupServices() + let (keyringService, rpcService, blockchainRegistry, assetRatioService, walletService) = setupServices() let mockAssetManager = TestableWalletUserAssetManager() mockAssetManager._getAllUserAssetsInNetworkAssets = { networks in var result: [NetworkAssets] = [] @@ -94,6 +97,7 @@ class UserAssetsStoreTests: XCTestCase { rpcService: rpcService, keyringService: keyringService, assetRatioService: assetRatioService, + walletService: walletService, ipfsApi: TestIpfsAPI(), userAssetManager: mockAssetManager ) @@ -143,7 +147,7 @@ class UserAssetsStoreTests: XCTestCase { } @MainActor func testUpdateWithNetworkFilter() async { - let (keyringService, rpcService, blockchainRegistry, assetRatioService) = setupServices() + let (keyringService, rpcService, blockchainRegistry, assetRatioService, walletService) = setupServices() let mockAssetManager = TestableWalletUserAssetManager() mockAssetManager._getAllUserAssetsInNetworkAssets = { networks in @@ -190,6 +194,7 @@ class UserAssetsStoreTests: XCTestCase { rpcService: rpcService, keyringService: keyringService, assetRatioService: assetRatioService, + walletService: walletService, ipfsApi: TestIpfsAPI(), userAssetManager: mockAssetManager )