From 5306d23c961974431f1403928cb65d017a61cf79 Mon Sep 17 00:00:00 2001 From: Nuo Xu Date: Thu, 7 Sep 2023 06:17:06 -0700 Subject: [PATCH] Fix #7809: Filecoin basic support on iOS (#7912) --- .../BrowserViewController+Wallet.swift | 2 +- .../Accounts/AccountSelectionView.swift | 11 +- .../Accounts/AccountTransactionListView.swift | 3 +- .../Crypto/Accounts/AccountsHeaderView.swift | 5 +- .../Activity/AccountActivityView.swift | 3 +- .../Crypto/Accounts/Add/AddAccountView.swift | 43 +- .../Accounts/Details/AccountDetailsView.swift | 2 +- .../Asset Details/AssetDetailView.swift | 8 +- .../Crypto/BuySendSwap/AccountPicker.swift | 1 + Sources/BraveWallet/Crypto/CryptoView.swift | 2 + .../Crypto/NetworkSelectionView.swift | 6 +- .../Crypto/Stores/AccountActivityStore.swift | 12 +- .../BraveWallet/Crypto/Stores/Address.swift | 11 + .../Crypto/Stores/AssetDetailStore.swift | 3 +- .../Crypto/Stores/CryptoStore.swift | 22 +- .../Crypto/Stores/KeyringStore.swift | 24 +- .../Stores/ManageSiteConnectionsStore.swift | 2 +- .../BraveWallet/Crypto/Stores/NFTStore.swift | 2 +- .../Crypto/Stores/NetworkStore.swift | 2 +- .../Crypto/Stores/PortfolioStore.swift | 12 +- .../Stores/SelectAccountTokenStore.swift | 24 +- .../Crypto/Stores/SendTokenStore.swift | 41 +- .../Crypto/Stores/SettingsStore.swift | 2 +- .../Stores/TransactionConfirmationStore.swift | 54 ++- .../Stores/TransactionDetailsStore.swift | 10 +- .../Stores/TransactionsActivityStore.swift | 8 +- .../PendingTransactionView.swift | 43 ++ .../TransactionStatusView.swift | 3 +- .../Transactions/TransactionDetailsView.swift | 3 +- .../Transactions/TransactionHeader.swift | 2 + ...TransactionParser+TransactionSummary.swift | 11 + .../Transactions/TransactionParser.swift | 163 ++++++-- .../Extensions/BraveWalletExtensions.swift | 51 ++- .../BraveWalletSwiftUIExtensions.swift | 4 + .../Extensions/KeyringServiceExtensions.swift | 34 +- .../Extensions/RpcServiceExtensions.swift | 46 ++- .../WalletTxServiceExtensions.swift | 16 +- .../Connect/EditSiteConnectionView.swift | 4 +- .../Connect/NewSiteConnectionView.swift | 4 +- .../BraveWallet/Panels/WalletPanelView.swift | 4 +- .../Preview Content/MockContent.swift | 48 +++ .../Preview Content/MockJsonRpcService.swift | 28 ++ .../Preview Content/MockKeyringService.swift | 22 ++ .../Settings/Web3SettingsView.swift | 2 +- Sources/BraveWallet/WalletConstants.swift | 16 +- Sources/BraveWallet/WalletStrings.swift | 7 + .../BraveWallet/WalletUserAssetManager.swift | 41 +- .../AccountActivityStoreTests.swift | 167 +++++++- Tests/BraveWalletTests/AddressTests.swift | 23 ++ .../BraveWalletTests/KeyringStoreTests.swift | 6 +- Tests/BraveWalletTests/NFTStoreTests.swift | 2 +- .../NetworkSelectionStoreTests.swift | 27 +- .../BraveWalletTests/NetworkStoreTests.swift | 16 +- .../PortfolioStoreTests.swift | 366 ++++++++++++++---- .../SelectAccountTokenStoreTests.swift | 148 ++++++- .../SendTokenStoreTests.swift | 102 ++++- .../TransactionConfirmationStoreTests.swift | 106 ++++- .../TransactionParserTests.swift | 113 +++++- .../TransactionsActivityStoreTests.swift | 79 ++-- .../UserAssetsStoreTests.swift | 30 +- 60 files changed, 1727 insertions(+), 325 deletions(-) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift index ea8123538a3..dc014bdd827 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift @@ -604,7 +604,7 @@ extension Tab: BraveWalletKeyringServiceObserver { // check domain already has some permitted accounts for this Tab's URLOrigin let permissionRequestManager = WalletProviderPermissionRequestsManager.shared let allAccounts = await keyringService.allAccounts().accounts - for coin in WalletConstants.supportedCoinTypes { + for coin in WalletConstants.supportedCoinTypes(.dapps) { let allAccountsForCoin = allAccounts.filter { $0.coin == coin } if permissionRequestManager.hasPendingRequest(for: origin, coinType: coin) { let pendingRequests = permissionRequestManager.pendingRequests(for: origin, coinType: coin) diff --git a/Sources/BraveWallet/Crypto/Accounts/AccountSelectionView.swift b/Sources/BraveWallet/Crypto/Accounts/AccountSelectionView.swift index 5b595d0193f..1691d05caaa 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountSelectionView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountSelectionView.swift @@ -11,6 +11,7 @@ import BraveUI /// Displays all accounts and will update the selected account to the account tapped on. struct AccountSelectionView: View { @ObservedObject var keyringStore: KeyringStore + var networkStore: NetworkStore let onDismiss: () -> Void @State private var isPresentingAddAccount: Bool = false @@ -47,7 +48,10 @@ struct AccountSelectionView: View { } .sheet(isPresented: $isPresentingAddAccount) { NavigationView { - AddAccountView(keyringStore: keyringStore) + AddAccountView( + keyringStore: keyringStore, + networkStore: networkStore + ) } .navigationViewStyle(.stack) } @@ -60,10 +64,11 @@ struct AccountSelectionView_Previews: PreviewProvider { AccountSelectionView( keyringStore: { let store = KeyringStore.previewStoreWithWalletCreated - store.addPrimaryAccount("Account 2", coin: .eth, completion: nil) - store.addPrimaryAccount("Account 3", coin: .eth, completion: nil) + store.addPrimaryAccount("Account 2", coin: .eth, chainId: BraveWallet.MainnetChainId, completion: nil) + store.addPrimaryAccount("Account 3", coin: .eth, chainId: BraveWallet.MainnetChainId, completion: nil) return store }(), + networkStore: .previewStore, onDismiss: {} ) } diff --git a/Sources/BraveWallet/Crypto/Accounts/AccountTransactionListView.swift b/Sources/BraveWallet/Crypto/Accounts/AccountTransactionListView.swift index 8930753e3b5..60dfb77c431 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountTransactionListView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountTransactionListView.swift @@ -44,8 +44,7 @@ struct AccountTransactionListView: View { if !txSummary.txHash.isEmpty { Button(action: { if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == txSummary.txInfo.chainId }), - let baseURL = txNetwork.blockExplorerUrls.first.map(URL.init(string:)), - let url = baseURL?.appendingPathComponent("tx/\(txSummary.txHash)") { + let url = txNetwork.txBlockExplorerLink(txHash: txSummary.txHash, for: txNetwork.coin) { openWalletURL(url) } }) { diff --git a/Sources/BraveWallet/Crypto/Accounts/AccountsHeaderView.swift b/Sources/BraveWallet/Crypto/Accounts/AccountsHeaderView.swift index ef27946cb96..a008f540200 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountsHeaderView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountsHeaderView.swift @@ -53,7 +53,10 @@ struct AccountsHeaderView: View { Color.clear .sheet(isPresented: $isPresentingAddAccount) { NavigationView { - AddAccountView(keyringStore: keyringStore) + AddAccountView( + keyringStore: keyringStore, + networkStore: networkStore + ) } .navigationViewStyle(StackNavigationViewStyle()) } diff --git a/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift b/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift index f8bcab186fb..da81f2c0a36 100644 --- a/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/Activity/AccountActivityView.swift @@ -122,8 +122,7 @@ struct AccountActivityView: View { if !txSummary.txHash.isEmpty { Button(action: { if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == txSummary.txInfo.chainId }), - let baseURL = txNetwork.blockExplorerUrls.first.map(URL.init(string:)), - let url = baseURL?.appendingPathComponent("tx/\(txSummary.txHash)") { + let url = txNetwork.txBlockExplorerLink(txHash: txSummary.txHash, for: txNetwork.coin) { openWalletURL(url) } }) { diff --git a/Sources/BraveWallet/Crypto/Accounts/Add/AddAccountView.swift b/Sources/BraveWallet/Crypto/Accounts/Add/AddAccountView.swift index 160f3aaa55c..8d29ce2b280 100644 --- a/Sources/BraveWallet/Crypto/Accounts/Add/AddAccountView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/Add/AddAccountView.swift @@ -19,16 +19,33 @@ struct AddAccountView: View { @State private var originPassword: String = "" @State private var failedToImport: Bool = false @State private var selectedCoin: BraveWallet.CoinType? + @State private var filNetwork: BraveWallet.NetworkInfo @ScaledMetric(relativeTo: .body) private var privateKeyFieldHeight: CGFloat = 140.0 @Environment(\.presentationMode) @Binding var presentationMode @Environment(\.appRatingRequestAction) private var appRatingRequest @ScaledMetric private var iconSize = 40.0 private let maxIconSize: CGFloat = 80.0 + private var allFilNetworks: [BraveWallet.NetworkInfo] var preSelectedCoin: BraveWallet.CoinType? var onCreate: (() -> Void)? var onDismiss: (() -> Void)? + + init( + keyringStore: KeyringStore, + networkStore: NetworkStore, + preSelectedCoin: BraveWallet.CoinType? = nil, + onCreate: (() -> Void)? = nil, + onDismiss: (() -> Void)? = nil + ) { + self.keyringStore = keyringStore + self.allFilNetworks = networkStore.allChains.filter { $0.coin == .fil } + self.preSelectedCoin = preSelectedCoin + self.onCreate = onCreate + self.onDismiss = onDismiss + _filNetwork = .init(initialValue: self.allFilNetworks.first ?? .init()) + } private func addAccount(for coin: BraveWallet.CoinType) { let accountName = name.isEmpty ? defaultAccountName(for: coin, isPrimary: privateKey.isEmpty) : name @@ -36,7 +53,7 @@ struct AddAccountView: View { if privateKey.isEmpty { // Add normal account - keyringStore.addPrimaryAccount(accountName, coin: coin) { success in + keyringStore.addPrimaryAccount(accountName, coin: coin, chainId: filNetwork.chainId) { success in if success { onCreate?() appRatingRequest?() @@ -56,7 +73,7 @@ struct AddAccountView: View { if isJSONImported { keyringStore.addSecondaryAccount(accountName, json: privateKey, password: originPassword, completion: handler) } else { - keyringStore.addSecondaryAccount(accountName, coin: coin, privateKey: privateKey, completion: handler) + keyringStore.addSecondaryAccount(accountName, coin: coin, chainId: filNetwork.chainId, privateKey: privateKey, completion: handler) } } } @@ -74,7 +91,7 @@ struct AddAccountView: View { } private var showCoinSelection: Bool { - preSelectedCoin == nil && WalletConstants.supportedCoinTypes.count > 1 + preSelectedCoin == nil && WalletConstants.supportedCoinTypes().count > 1 } private var navigationTitle: String { @@ -86,6 +103,19 @@ struct AddAccountView: View { @ViewBuilder private var addAccountView: some View { List { + if (selectedCoin == .fil || preSelectedCoin == .fil) && !allFilNetworks.isEmpty { + Picker(selection: $filNetwork) { + ForEach(allFilNetworks) { network in + Text(network.chainName) + .foregroundColor(Color(.secondaryBraveLabel)) + .tag(network) + } + } label: { + Text(Strings.Wallet.transactionDetailsNetworkTitle) + .foregroundColor(Color(.braveLabel)) + } + .listRowBackground(Color(.secondaryBraveGroupedBackground)) + } accountNameSection if isJSONImported { originPasswordSection @@ -122,7 +152,7 @@ struct AddAccountView: View { title: Text(Strings.Wallet.coinTypeSelectionHeader) ) ) { - ForEach(Array(WalletConstants.supportedCoinTypes)) { coin in + ForEach(WalletConstants.supportedCoinTypes().elements) { coin in NavigationLink( tag: coin, selection: $selectedCoin) { @@ -310,7 +340,10 @@ struct AddAccountView: View { struct AddAccountView_Previews: PreviewProvider { static var previews: some View { NavigationView { - AddAccountView(keyringStore: .previewStore) + AddAccountView( + keyringStore: .previewStore, + networkStore: .previewStore + ) } } } diff --git a/Sources/BraveWallet/Crypto/Accounts/Details/AccountDetailsView.swift b/Sources/BraveWallet/Crypto/Accounts/Details/AccountDetailsView.swift index a10c1eaf31b..b8bdf607789 100644 --- a/Sources/BraveWallet/Crypto/Accounts/Details/AccountDetailsView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/Details/AccountDetailsView.swift @@ -181,7 +181,7 @@ private struct AccountDetailsHeaderView: View { } #if DEBUG -struct AccountDetailsViewController_Previews: PreviewProvider { +struct AccountDetailsView_Previews: PreviewProvider { static var previews: some View { AccountDetailsView( keyringStore: .previewStoreWithWalletCreated, diff --git a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift index 28bab25df3c..cbf19c78ff8 100644 --- a/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift +++ b/Sources/BraveWallet/Crypto/Asset Details/AssetDetailView.swift @@ -104,8 +104,7 @@ struct AssetDetailView: View { if !txSummary.txHash.isEmpty { Button(action: { if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == txSummary.txInfo.chainId }), - let baseURL = txNetwork.blockExplorerUrls.first.map(URL.init(string:)), - let url = baseURL?.appendingPathComponent("tx/\(txSummary.txHash)") { + let url = txNetwork.txBlockExplorerLink(txHash: txSummary.txHash, for: txNetwork.coin) { openWalletURL(url) } }) { @@ -211,7 +210,10 @@ struct AssetDetailView: View { Color.clear .sheet(isPresented: $isShowingAddAccount) { NavigationView { - AddAccountView(keyringStore: keyringStore) + AddAccountView( + keyringStore: keyringStore, + networkStore: networkStore + ) } .navigationViewStyle(StackNavigationViewStyle()) } diff --git a/Sources/BraveWallet/Crypto/BuySendSwap/AccountPicker.swift b/Sources/BraveWallet/Crypto/BuySendSwap/AccountPicker.swift index b555066e952..b753e427f30 100644 --- a/Sources/BraveWallet/Crypto/BuySendSwap/AccountPicker.swift +++ b/Sources/BraveWallet/Crypto/BuySendSwap/AccountPicker.swift @@ -35,6 +35,7 @@ struct AccountPicker: View { NavigationView { AccountSelectionView( keyringStore: keyringStore, + networkStore: networkStore, onDismiss: { isPresentingPicker = false } ) } diff --git a/Sources/BraveWallet/Crypto/CryptoView.swift b/Sources/BraveWallet/Crypto/CryptoView.swift index c066aed68e0..0f42cfbda18 100644 --- a/Sources/BraveWallet/Crypto/CryptoView.swift +++ b/Sources/BraveWallet/Crypto/CryptoView.swift @@ -120,6 +120,7 @@ public struct CryptoView: View { NavigationView { AccountSelectionView( keyringStore: keyringStore, + networkStore: store.networkStore, onDismiss: { dismissAction() } @@ -212,6 +213,7 @@ public struct CryptoView: View { NavigationView { AddAccountView( keyringStore: keyringStore, + networkStore: store.networkStore, preSelectedCoin: request.coinType, onCreate: { // request is fullfilled. diff --git a/Sources/BraveWallet/Crypto/NetworkSelectionView.swift b/Sources/BraveWallet/Crypto/NetworkSelectionView.swift index 47b4dd61737..731b78be316 100644 --- a/Sources/BraveWallet/Crypto/NetworkSelectionView.swift +++ b/Sources/BraveWallet/Crypto/NetworkSelectionView.swift @@ -76,7 +76,11 @@ struct NetworkSelectionView: View { isPresented: $store.isPresentingAddAccount ) { NavigationView { - AddAccountView(keyringStore: keyringStore, preSelectedCoin: store.nextNetwork?.coin) + AddAccountView( + keyringStore: keyringStore, + networkStore: networkStore, + preSelectedCoin: store.nextNetwork?.coin + ) } .navigationViewStyle(.stack) .onDisappear { diff --git a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift index 1cff800d24b..8369fa7574a 100644 --- a/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift @@ -78,8 +78,16 @@ class AccountActivityStore: ObservableObject { let coin = account.coin let networksForAccountCoin = await rpcService.allNetworks(coin) .filter { $0.chainId != BraveWallet.LocalhostChainId } // localhost not supported + let networksForAccount = networksForAccountCoin.filter { // .fil coin type has two different keyring ids + $0.supportedKeyrings.contains(account.keyringId.rawValue as NSNumber) + } - let allVisibleUserAssets = assetManager.getAllVisibleAssetsInNetworkAssets(networks: networksForAccountCoin) + struct NetworkAssets: Equatable { + let network: BraveWallet.NetworkInfo + let tokens: [BraveWallet.BlockchainToken] + let sortOrder: Int + } + let allVisibleUserAssets = assetManager.getAllVisibleAssetsInNetworkAssets(networks: networksForAccount) let allTokens = await blockchainRegistry.allTokens(in: networksForAccountCoin).flatMap(\.tokens) var updatedUserVisibleAssets: [AssetViewModel] = [] var updatedUserVisibleNFTs: [NFTAssetViewModel] = [] @@ -110,7 +118,7 @@ class AccountActivityStore: ObservableObject { self.userVisibleAssets = updatedUserVisibleAssets self.userVisibleNFTs = updatedUserVisibleNFTs - let keyringForAccount = await keyringService.keyringInfo(coin.keyringId) + let keyringForAccount = await keyringService.keyringInfo(account.keyringId) typealias TokenNetworkAccounts = (token: BraveWallet.BlockchainToken, network: BraveWallet.NetworkInfo, accounts: [BraveWallet.AccountInfo]) let allTokenNetworkAccounts = allVisibleUserAssets.flatMap { networkAssets in networkAssets.tokens.map { token in diff --git a/Sources/BraveWallet/Crypto/Stores/Address.swift b/Sources/BraveWallet/Crypto/Stores/Address.swift index 4859e7153b1..d5d85d1a075 100644 --- a/Sources/BraveWallet/Crypto/Stores/Address.swift +++ b/Sources/BraveWallet/Crypto/Stores/Address.swift @@ -4,6 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation +import BraveCore extension String { /// Truncates an address to only show the first 4 digits and last 4 digits @@ -43,6 +44,16 @@ extension String { return hex.count == 40 && hex.allSatisfy(\.isHexDigit) } + /// Check if the string is a valid FIL address + var isFILAddress: Bool { + if starts(with: BraveWallet.FilecoinMainnet) || starts(with: BraveWallet.FilecoinTestnet) {// FIL address has to start with `f` or `t` + if count == 41 || count == 86 || count == 44 { // secp256k have 41 address length and BLS keys have 86 and FEVM f410 keys have 44 + return true + } + } + return false + } + /// Strip prefix if it exists, ex. 'ethereum:' var strippedETHAddress: String { guard !isETHAddress else { return self } diff --git a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift index a9471112e2d..0b7b7859f03 100644 --- a/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift @@ -158,7 +158,8 @@ class AssetDetailStore: ObservableObject { self.isSwapSupported = await swapService.isSwapSupported(token.chainId) // fetch accounts - let keyring = await keyringService.keyringInfo(token.coin.keyringId) + let keyringId = BraveWallet.KeyringId.keyringId(for: token.coin, on: token.chainId) + let keyring = await keyringService.keyringInfo(keyringId) var updatedAccounts = keyring.accountInfos.map { AccountAssetViewModel(account: $0, decimalBalance: 0.0, balance: "", fiatBalance: "") } diff --git a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift index d77c7613b1b..cfe73bf338d 100644 --- a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift @@ -410,13 +410,13 @@ public class CryptoStore: ObservableObject { @MainActor func fetchPendingTransactions() async -> [BraveWallet.TransactionInfo] { - let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes) - var allChainIdsForCoin: [BraveWallet.CoinType: [String]] = [:] - for coin in WalletConstants.supportedCoinTypes { + let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes()) + var allNetworksForCoin: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [:] + for coin in WalletConstants.supportedCoinTypes() { let allNetworks = await rpcService.allNetworks(coin) - allChainIdsForCoin[coin] = allNetworks.map(\.chainId) + allNetworksForCoin[coin] = allNetworks } - return await txService.pendingTransactions(chainIdsForCoin: allChainIdsForCoin, for: allKeyrings) + return await txService.pendingTransactions(networksForCoin: allNetworksForCoin, for: allKeyrings) } @MainActor @@ -560,13 +560,11 @@ extension CryptoStore: BraveWalletKeyringServiceObserver { rejectAllPendingWebpageRequests() } public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { - Task { @MainActor [weak self] in - if let newCoin = WalletConstants.supportedCoinTypes.first(where: { $0.keyringId == keyringId }) { - self?.userAssetManager.migrateUserAssets(for: newCoin, completion: { - self?.updateAssets() - }) - } - } + // 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 diff --git a/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift b/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift index 34aa94d50ea..a341257cd23 100644 --- a/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift @@ -222,7 +222,7 @@ public class KeyringStore: ObservableObject { Task { @MainActor in // fetch all KeyringInfo for all coin types let selectedAccount = await keyringService.allAccounts().selectedAccount let selectedAccountAddress = selectedAccount?.address - let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes) + let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes()) if let defaultKeyring = allKeyrings.first(where: { $0.id == BraveWallet.KeyringId.default }) { self.defaultKeyring = defaultKeyring self.isDefaultKeyringLoaded = true @@ -356,7 +356,7 @@ public class KeyringStore: ObservableObject { self.updateKeyringInfo() self.resetKeychainStoredPassword() } - for coin in WalletConstants.supportedCoinTypes { + for coin in WalletConstants.supportedCoinTypes(.dapps) { // only coin types support dapps have permission management Domain.clearAllWalletPermissions(for: coin) } Preferences.Wallet.sortOrderFilter.reset() @@ -369,10 +369,17 @@ public class KeyringStore: ObservableObject { } } - func addPrimaryAccount(_ name: String, coin: BraveWallet.CoinType, completion: ((Bool) -> Void)? = nil) { + /// `chainId` is only for .fil or .btc coin type + /// correct `BraveWallet.KeyringId` will be returned from `keyringIdForNewAccount` + func addPrimaryAccount( + _ name: String, + coin: BraveWallet.CoinType, + chainId: String, + completion: ((Bool) -> Void)? = nil + ) { keyringService.addAccount( coin, - keyringId: coin.keyringId, + keyringId: BraveWallet.KeyringId.keyringId(for: coin, on: chainId), accountName: name ) { accountInfo in self.updateKeyringInfo() @@ -380,18 +387,17 @@ public class KeyringStore: ObservableObject { } } + /// `chainId` is only for .fil or .btc coin type func addSecondaryAccount( _ name: String, coin: BraveWallet.CoinType, + chainId: String, privateKey: String, completion: ((BraveWallet.AccountInfo?) -> Void)? = nil ) { if coin == .fil { - rpcService.network(.fil, origin: nil) { [self] defaultNetwork in - let networkId = defaultNetwork.chainId.caseInsensitiveCompare(BraveWallet.FilecoinMainnet) == .orderedSame ? BraveWallet.FilecoinMainnet : BraveWallet.FilecoinTestnet - keyringService.importFilecoinAccount(name, privateKey: privateKey, network: networkId) { accountInfo in - completion?(accountInfo) - } + keyringService.importFilecoinAccount(name, privateKey: privateKey, network: chainId) { accountInfo in + completion?(accountInfo) } } else { keyringService.importAccount(name, privateKey: privateKey, coin: coin) { accountInfo in diff --git a/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift b/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift index 6c3b314f788..d189197781e 100644 --- a/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/ManageSiteConnectionsStore.swift @@ -41,7 +41,7 @@ class ManageSiteConnectionsStore: ObservableObject { /// Fetch all site connections with 1+ accounts connected func fetchSiteConnections() { var connections = [SiteConnection]() - for coin in WalletConstants.supportedCoinTypes { + for coin in WalletConstants.supportedCoinTypes(.dapps) { // only coin types support dapps have site connection screen let domains = Domain.allDomainsWithWalletPermissions(for: coin) connections.append(contentsOf: domains.map { var connectedAddresses = [String]() diff --git a/Sources/BraveWallet/Crypto/Stores/NFTStore.swift b/Sources/BraveWallet/Crypto/Stores/NFTStore.swift index 79f79474d41..baee7d745d1 100644 --- a/Sources/BraveWallet/Crypto/Stores/NFTStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NFTStore.swift @@ -115,7 +115,7 @@ public class NFTStore: ObservableObject { self.updateTask = Task { @MainActor in self.allAccounts = await keyringService.allAccounts().accounts .filter { account in - WalletConstants.supportedCoinTypes.contains(account.coin) + WalletConstants.supportedCoinTypes().contains(account.coin) } self.allNetworks = await rpcService.allNetworksForSupportedCoins() let filters = self.filters diff --git a/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift b/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift index 89cfef3031e..4cf0b68840e 100644 --- a/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NetworkStore.swift @@ -109,7 +109,7 @@ public class NetworkStore: ObservableObject { _ network: BraveWallet.NetworkInfo, isForOrigin: Bool ) async -> SetSelectedChainError? { - let keyringId = network.coin.keyringId + let keyringId = BraveWallet.KeyringId.keyringId(for: network.coin, on: network.chainId) let keyringInfo = await keyringService.keyringInfo(keyringId) if keyringInfo.accountInfos.isEmpty { // Need to prompt user to create new account via alert diff --git a/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift b/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift index 88ecfc4c32f..85a41f192d7 100644 --- a/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/PortfolioStore.swift @@ -334,7 +334,7 @@ public class PortfolioStore: ObservableObject { self.isLoadingBalances = true self.allAccounts = await keyringService.allAccounts().accounts .filter { account in - WalletConstants.supportedCoinTypes.contains(account.coin) + WalletConstants.supportedCoinTypes().contains(account.coin) } self.allNetworks = await rpcService.allNetworksForSupportedCoins() let filters = self.filters @@ -355,7 +355,13 @@ public class PortfolioStore: ObservableObject { TokenNetworkAccounts( token: token, network: networkAssets.network, - accounts: selectedAccounts.filter { $0.coin == token.coin } + accounts: selectedAccounts.filter { + if token.coin == .fil { + return $0.keyringId == BraveWallet.KeyringId.keyringId(for: token.coin, on: token.chainId) + } else { + return $0.coin == token.coin + } + } ) } } @@ -564,7 +570,7 @@ public class PortfolioStore: ObservableObject { }) case let .account(account): return allVisibleUserAssets - .filter { $0.network.coin == account.coin } + .filter { $0.network.coin == account.coin && $0.network.supportedKeyrings.contains(account.accountId.keyringId.rawValue as NSNumber) } .flatMap { networkAssets in networkAssets.tokens.map { token in AssetViewModel( diff --git a/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift b/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift index edd1fa0cd98..ef92e8514cc 100644 --- a/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SelectAccountTokenStore.swift @@ -143,7 +143,7 @@ class SelectAccountTokenStore: ObservableObject { } @MainActor func update() async { - let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes) + let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes()) let allNetworks = await rpcService.allNetworksForSupportedCoins() self.allNetworks = allNetworks // setup network filters if not currently setup @@ -157,14 +157,18 @@ class SelectAccountTokenStore: ObservableObject { self.accountSections = allKeyrings.flatMap { keyring in let tokensForCoin = allVisibleUserAssets.filter { $0.coin == keyring.coin } return keyring.accountInfos.map { account in - let tokenBalances = tokensForCoin.map { token in - AccountSection.TokenBalance( - token: token, - network: allNetworks.first(where: { $0.chainId == token.chainId }) ?? .init(), - balance: cachedBalance(for: token, in: account), - price: cachedPrice(for: token, in: account), - nftMetadata: cachedMetadata(for: token) - ) + let tokenBalances = tokensForCoin.compactMap { token in + let tokenNetwork = allNetworks.first(where: { $0.chainId == token.chainId }) ?? .init() + if tokenNetwork.supportedKeyrings.contains(keyring.id.rawValue as NSNumber) { + return AccountSection.TokenBalance( + token: token, + network: allNetworks.first(where: { $0.chainId == token.chainId }) ?? .init(), + balance: cachedBalance(for: token, in: account), + price: cachedPrice(for: token, in: account), + nftMetadata: cachedMetadata(for: token) + ) + } + return nil } return AccountSection( account: account, @@ -306,7 +310,7 @@ class SelectAccountTokenStore: ObservableObject { #if DEBUG extension SelectAccountTokenStore { func setupForTesting() { - allNetworks = [.mockMainnet, .mockGoerli, .mockSolana, .mockSolanaTestnet] + allNetworks = [.mockMainnet, .mockGoerli, .mockSolana, .mockSolanaTestnet, .mockFilecoinMainnet, .mockFilecoinTestnet] } } #endif diff --git a/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift b/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift index 02039c66c14..83cec0224e5 100644 --- a/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SendTokenStore.swift @@ -78,6 +78,7 @@ public class SendTokenStore: ObservableObject { case snsError(domain: String) case ensError(domain: String) case udError(domain: String) + case notFilAddress var errorDescription: String? { switch self { @@ -99,6 +100,8 @@ public class SendTokenStore: ObservableObject { return String.localizedStringWithFormat(Strings.Wallet.sendErrorDomainNotRegistered, BraveWallet.CoinType.eth.localizedTitle) case .udError: return String.localizedStringWithFormat(Strings.Wallet.sendErrorDomainNotRegistered, BraveWallet.CoinType.eth.localizedTitle) + case .notFilAddress: + return Strings.Wallet.sendErrorInvalidRecipientAddress } } @@ -311,7 +314,9 @@ public class SendTokenStore: ObservableObject { await validateEthereumSendAddress(fromAddress: selectedAccount.address) case .sol: await validateSolanaSendAddress(fromAddress: selectedAccount.address) - case .fil, .btc: + case .fil: + validateFilcoinSendAddress() + case .btc: break @unknown default: break @@ -407,6 +412,10 @@ public class SendTokenStore: ObservableObject { addressError = nil } + private func validateFilcoinSendAddress() { + addressError = sendAddress.isFILAddress ? nil : .notFilAddress + } + public func enableENSOffchainLookup() { Task { @MainActor in rpcService.setEnsOffchainLookupResolveMethod(.enabled) @@ -488,6 +497,8 @@ public class SendTokenStore: ObservableObject { self.sendTokenOnEth(amount: amount, token: token, fromAddress: selectedAccount.address, completion: completion) case .sol: self.sendTokenOnSol(amount: amount, token: token, fromAddress: selectedAccount.address, completion: completion) + case .fil: + self.sendTokenOnFil(amount: amount, token: token, fromAddress: selectedAccount.address, completion: completion) default: completion(false, Strings.Wallet.internalErrorMessage) } @@ -613,6 +624,34 @@ public class SendTokenStore: ObservableObject { } } + private func sendTokenOnFil( + amount: String, + token: BraveWallet.BlockchainToken, + fromAddress: String, + completion: @escaping (_ success: Bool, _ errMsg: String?) -> Void + ) { + let weiFormatter = WeiFormatter(decimalFormatStyle: .decimals(precision: Int(token.decimals))) + guard let weiString = weiFormatter.weiString(from: amount.normalizedDecimals, decimals: Int(token.decimals)) else { + completion(false, Strings.Wallet.internalErrorMessage) + return + } + + isMakingTx = true + let filTxData = BraveWallet.FilTxData( + nonce: "", + gasPremium: "", + gasFeeCap: "", + gasLimit: "", + maxFee: "0", + to: sendAddress, + value: weiString + ) + self.txService.addUnapprovedTransaction(BraveWallet.TxDataUnion(filTxData: filTxData), from: fromAddress, origin: nil, groupId: nil) { success, txMetaId, errorMessage in + self.isMakingTx = false + completion(success, errorMessage) + } + } + @MainActor func fetchNFTMetadata(tokens: [BraveWallet.BlockchainToken]) async -> [String: NFTMetadata] { return await rpcService.fetchNFTMetadata(tokens: tokens, ipfsApi: ipfsApi) } diff --git a/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift b/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift index 0bd7392cac8..e72736d8e4c 100644 --- a/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/SettingsStore.swift @@ -121,7 +121,7 @@ public class SettingsStore: ObservableObject { walletService.reset() keychain.resetPasswordInKeychain(key: KeyringStore.passwordKeychainKey) - for coin in WalletConstants.supportedCoinTypes { + for coin in WalletConstants.supportedCoinTypes() { Domain.clearAllWalletPermissions(for: coin) Preferences.Wallet.reset(for: coin) } diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift index 3bd1251aeef..f891eb4a27b 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionConfirmationStore.swift @@ -42,6 +42,12 @@ public class TransactionConfirmationStore: ObservableObject { @Published var transactionDetails: String = "" /// The gas esitimation for this eip1559 transaction @Published var eip1559GasEstimation: BraveWallet.GasEstimation1559? + /// The gas premium for filcoin transaction + @Published var filTxGasPremium: String? + /// The gas limit for filcoin transaction + @Published var filTxGasLimit: String? + /// The gas fee cap for filcoin transaction + @Published var filTxGasFeeCap: String? /// The origin info of this transaction @Published var originInfo: BraveWallet.OriginInfo? /// This is an id for the unppproved transaction that is currently displayed on screen @@ -218,7 +224,8 @@ public class TransactionConfirmationStore: ObservableObject { clearTrasactionInfoBeforeUpdate() let coin = transaction.coin - let keyring = await keyringService.keyringInfo(coin.keyringId) + let keyringId = BraveWallet.KeyringId.keyringId(for: transaction.coin, on: transaction.chainId) + let keyring = await keyringService.keyringInfo(keyringId) if !allNetworks.contains(where: { $0.chainId == transaction.chainId }) { allNetworks = await rpcService.allNetworksForSupportedCoins() } @@ -285,6 +292,10 @@ public class TransactionConfirmationStore: ObservableObject { isBalanceSufficient = true isSolTokenTransferWithAssociatedTokenAccountCreation = false isUnlimitedApprovalRequested = false + // Filecoin Tx + filTxGasPremium = nil + filTxGasLimit = nil + filTxGasFeeCap = nil } private var assetRatios: [String: Double] = [:] @@ -533,6 +544,37 @@ public class TransactionConfirmationStore: ObservableObject { } } } + case let .filSend(details): + symbol = details.sendToken?.symbol ?? "" + value = details.sendAmount + fiat = details.sendFiat ?? "" + + filTxGasPremium = details.gasPremium + filTxGasLimit = details.gasLimit + filTxGasFeeCap = details.gasFeeCap + + if let gasFee = details.gasFee { + gasValue = gasFee.fee + gasFiat = gasFee.fiat + gasSymbol = activeParsedTransaction.networkSymbol + gasAssetRatio = assetRatios[activeParsedTransaction.networkSymbol.lowercased(), default: 0] + + if let gasBalance = gasTokenBalanceCache["\(network.nativeToken.symbol)\(activeParsedTransaction.fromAddress)"] { + if let gasValue = BDouble(gasFee.fee), + BDouble(gasBalance) > gasValue { + isBalanceSufficient = true + } else { + isBalanceSufficient = false + } + } else if shouldFetchGasTokenBalance { + if let account = keyring.accountInfos.first(where: { $0.address == activeParsedTransaction.fromAddress }) { + await fetchGasTokenBalance(token: network.nativeToken, account: account, network: network) + } + } + } + if let token = details.sendToken { + totalFiat = totalFiat(value: value, tokenAssetRatioId: token.assetRatioId, gasValue: gasValue, gasSymbol: gasSymbol, assetRatios: assetRatios, currencyFormatter: currencyFormatter) + } case .other: break } @@ -555,13 +597,13 @@ public class TransactionConfirmationStore: ObservableObject { } @MainActor private func fetchAllTransactions() async -> [BraveWallet.TransactionInfo] { - let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes) - var allChainIdsForCoin: [BraveWallet.CoinType: [String]] = [:] - for coin in WalletConstants.supportedCoinTypes { + let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes()) + var allNetworksForCoin: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [:] + for coin in WalletConstants.supportedCoinTypes() { let allNetworks = await rpcService.allNetworks(coin) - allChainIdsForCoin[coin] = allNetworks.map(\.chainId) + allNetworksForCoin[coin] = allNetworks } - return await txService.pendingTransactions(chainIdsForCoin: allChainIdsForCoin, for: allKeyrings) + return await txService.pendingTransactions(networksForCoin: allNetworksForCoin, for: allKeyrings) .sorted(by: { $0.createdTime > $1.createdTime }) } diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift index 46a5a30c2ba..7dda58072ae 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift @@ -75,7 +75,8 @@ class TransactionDetailsStore: ObservableObject { return } self.network = network - let keyring = await keyringService.keyringInfo(coin.keyringId) + let keringId = BraveWallet.KeyringId.keyringId(for: coin, on: transaction.chainId) + let keyring = await keyringService.keyringInfo(keringId) var allTokens: [BraveWallet.BlockchainToken] = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) + tokenInfoCache.map(\.value) let userVisibleTokens: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network]).flatMap { $0.tokens } let unknownTokenContractAddresses = transaction.tokenContractAddresses @@ -167,6 +168,13 @@ class TransactionDetailsStore: ObservableObject { case let .solSwapTransaction(details): self.title = Strings.Wallet.solanaSwapTransactionTitle self.value = details.fromAmount + case let .filSend(details): + self.title = Strings.Wallet.sent + self.value = String(format: "%@ %@", details.sendAmount, details.sendToken?.symbol ?? "") + self.fiat = details.sendFiat + if let sendToken = details.sendToken, let tokenPrice = assetRatios[sendToken.assetRatioId.lowercased()] { + self.marketPrice = currencyFormatter.string(from: NSNumber(value: tokenPrice)) ?? "$0.00" + } case .other: break } diff --git a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift index fd06934e1ab..ef76d186889 100644 --- a/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift @@ -70,7 +70,7 @@ class TransactionsActivityStore: ObservableObject { updateTask?.cancel() updateTask = Task { @MainActor in let allKeyrings = await self.keyringService.keyrings( - for: WalletConstants.supportedCoinTypes + for: WalletConstants.supportedCoinTypes() ) let allAccountInfos = allKeyrings.flatMap(\.accountInfos) // setup network filters if not currently setup @@ -81,14 +81,10 @@ class TransactionsActivityStore: ObservableObject { } let networks = networkFilters.filter(\.isSelected).map(\.model) let networksForCoin: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = Dictionary(grouping: networks, by: \.coin) - - let chainIdsForCoin = networksForCoin.mapValues { networks in - networks.map(\.chainId) - } let allNetworksAllCoins = networksForCoin.values.flatMap { $0 } let allTransactions = await txService.allTransactions( - chainIdsForCoin: chainIdsForCoin, for: allKeyrings + networksForCoin: networksForCoin, for: allKeyrings ).filter { $0.txStatus != .rejected } let userVisibleTokens = assetManager.getAllVisibleAssetsInNetworkAssets(networks: allNetworksAllCoins).flatMap(\.tokens) let allTokens = await blockchainRegistry.allTokens( diff --git a/Sources/BraveWallet/Crypto/Transaction Confirmations/PendingTransactionView.swift b/Sources/BraveWallet/Crypto/Transaction Confirmations/PendingTransactionView.swift index af25119b5f3..d36070f305c 100644 --- a/Sources/BraveWallet/Crypto/Transaction Confirmations/PendingTransactionView.swift +++ b/Sources/BraveWallet/Crypto/Transaction Confirmations/PendingTransactionView.swift @@ -263,6 +263,47 @@ struct PendingTransactionView: View { switch viewMode { case .transaction: VStack(spacing: 0) { + if confirmationStore.activeParsedTransaction.coin == .fil { + if let gasLimit = confirmationStore.filTxGasLimit { + HStack { + Text("Gas Limit") + .foregroundColor(Color(.bravePrimary)) + Spacer() + Text("\(gasLimit) \(confirmationStore.gasSymbol)") + .foregroundColor(Color(.bravePrimary)) + .multilineTextAlignment(.trailing) + } + .padding() + .accessibilityElement(children: .contain) + Divider() + } + if let gasPremium = confirmationStore.filTxGasPremium { + HStack { + Text("Gas Premium") + .foregroundColor(Color(.bravePrimary)) + Spacer() + Text("\(gasPremium) \(confirmationStore.gasSymbol)") + .foregroundColor(Color(.bravePrimary)) + .multilineTextAlignment(.trailing) + } + .padding() + .accessibilityElement(children: .contain) + Divider() + } + if let gasFeeCap = confirmationStore.filTxGasFeeCap { + HStack { + Text("Gas Fee Cap") + .foregroundColor(Color(.bravePrimary)) + Spacer() + Text("\(gasFeeCap) \(confirmationStore.gasSymbol)") + .foregroundColor(Color(.bravePrimary)) + .multilineTextAlignment(.trailing) + } + .padding() + .accessibilityElement(children: .contain) + Divider() + } + } HStack { VStack(alignment: .leading) { Text(confirmationStore.activeParsedTransaction.coin == .sol ? Strings.Wallet.transactionFee : Strings.Wallet.gasFee) @@ -275,6 +316,7 @@ struct PendingTransactionView: View { VStack(alignment: .trailing) { Text("\(confirmationStore.gasValue) \(confirmationStore.gasSymbol)") .foregroundColor(Color(.bravePrimary)) + .multilineTextAlignment(.trailing) Text(confirmationStore.gasFiat) .font(.footnote) } @@ -319,6 +361,7 @@ struct PendingTransactionView: View { .foregroundColor(Color(.secondaryBraveLabel)) Text("\(confirmationStore.value) \(confirmationStore.symbol) + \(confirmationStore.gasValue) \(confirmationStore.gasSymbol)") .foregroundColor(Color(.bravePrimary)) + .multilineTextAlignment(.trailing) HStack(spacing: 4) { if !confirmationStore.isBalanceSufficient { Text(Strings.Wallet.insufficientBalance) diff --git a/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionStatusView.swift b/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionStatusView.swift index ef0fd0f12e7..56e01d9f68a 100644 --- a/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionStatusView.swift +++ b/Sources/BraveWallet/Crypto/Transaction Confirmations/TransactionStatusView.swift @@ -41,8 +41,7 @@ struct TransactionStatusView: View { Button { if let tx = confirmationStore.allTxs.first(where: { $0.id == confirmationStore.activeTransactionId }), let txNetwork = networkStore.allChains.first(where: { $0.chainId == tx.chainId }), - let baseURL = txNetwork.blockExplorerUrls.first.map(URL.init(string:)), - let url = baseURL?.appendingPathComponent("tx/\(tx.txHash)") { + let url = txNetwork.txBlockExplorerLink(txHash: tx.txHash, for: txNetwork.coin) { openWalletURL(url) } } label: { diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift index e18a4f69664..825d5bc4a15 100644 --- a/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift +++ b/Sources/BraveWallet/Crypto/Transactions/TransactionDetailsView.swift @@ -62,8 +62,7 @@ struct TransactionDetailsView: View { if !transactionDetailsStore.transaction.txHash.isEmpty { Button(action: { if let txNetwork = self.networkStore.allChains.first(where: { $0.chainId == transactionDetailsStore.transaction.chainId }), - let baseURL = txNetwork.blockExplorerUrls.first.map(URL.init(string:)), - let url = baseURL?.appendingPathComponent("tx/\(transactionDetailsStore.transaction.txHash)") { + let url = txNetwork.txBlockExplorerLink(txHash: transactionDetailsStore.transaction.txHash, for: txNetwork.coin) { openWalletURL(url) } }) { diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionHeader.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionHeader.swift index 2234a99427c..2eda8d415bc 100644 --- a/Sources/BraveWallet/Crypto/Transactions/TransactionHeader.swift +++ b/Sources/BraveWallet/Crypto/Transactions/TransactionHeader.swift @@ -53,10 +53,12 @@ struct TransactionHeader: View { AddressView(address: fromAccountAddress) { Text(fromAccountName) } + .frame(minWidth: 0, maxWidth: .infinity) Image(systemName: "arrow.right") AddressView(address: toAccountAddress) { Text(toAccountName) } + .frame(minWidth: 0, maxWidth: .infinity) } } } diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift index cd334766449..746572b5430 100644 --- a/Sources/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift +++ b/Sources/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift @@ -179,6 +179,17 @@ extension TransactionParser { gasFee: parsedTransaction.gasFee, networkSymbol: parsedTransaction.networkSymbol ) + case let .filSend(details): + let title = String.localizedStringWithFormat(Strings.Wallet.transactionSendTitle, details.sendAmount, details.sendToken?.symbol ?? "", details.sendFiat ?? "") + return .init( + txInfo: transaction, + namedFromAddress: parsedTransaction.namedFromAddress, + toAddress: parsedTransaction.toAddress, + namedToAddress: parsedTransaction.namedToAddress, + title: title, + gasFee: details.gasFee, + networkSymbol: parsedTransaction.networkSymbol + ) case .other: return .init(txInfo: .init(), namedFromAddress: "", toAddress: "", namedToAddress: "", title: "", gasFee: nil, networkSymbol: "") } diff --git a/Sources/BraveWallet/Crypto/Transactions/TransactionParser.swift b/Sources/BraveWallet/Crypto/Transactions/TransactionParser.swift index 3f52c616708..915ff5ac4be 100644 --- a/Sources/BraveWallet/Crypto/Transactions/TransactionParser.swift +++ b/Sources/BraveWallet/Crypto/Transactions/TransactionParser.swift @@ -50,7 +50,30 @@ enum TransactionParser { gasFee = .init(fee: value, fiat: "$0.00") } } - case .fil, .btc: + case .fil: + guard let filTxData = transaction.txDataUnion.filTxData else { return nil } + if let gasLimit = BDouble(filTxData.gasLimit), + let gasFeeCapValue = BDouble(filTxData.gasFeeCap), + let gasPremiumValue = BDouble(filTxData.gasPremium) { + let decimals = Int(network.decimals) + // baseFee = gasFeeCap - gasPremium + let baseFeeValue = gasFeeCapValue - gasPremiumValue + let gasFeeValue = (transaction.isEIP1559Transaction ? gasFeeCapValue : baseFeeValue) * gasLimit + let gasFeeValueDecimals = gasFeeValue / (BDouble(10) ** decimals) + + let gasFeeString = gasFeeValueDecimals.decimalExpansion( + precisionAfterDecimalPoint: decimals, + rounded: true + ).trimmingTrailingZeros + if let doubleValue = Double(gasFeeString), + let assetRatio = assetRatios[network.symbol.lowercased()], + let fiat = currencyFormatter.string(from: NSNumber(value: doubleValue * assetRatio)) { + gasFee = .init(fee: gasFeeString, fiat: fiat) + } else { + gasFee = .init(fee: gasFeeString, fiat: "$0.00") + } + } + case .btc: break @unknown default: break @@ -87,40 +110,91 @@ enum TransactionParser { let formatter = WeiFormatter(decimalFormatStyle: decimalFormatStyle ?? .decimals(precision: Int(network.decimals))) switch transaction.txType { case .ethSend, .other: - let fromValue = transaction.ethTxValue - let fromValueFormatted = formatter.decimalString(for: fromValue.removingHexPrefix, radix: .hex, decimals: Int(network.decimals))?.trimmingTrailingZeros ?? "" - let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[network.nativeToken.assetRatioId.lowercased(), default: 0] * (Double(fromValueFormatted) ?? 0))) ?? "$0.00" - /* Example: - Send 0.1234 ETH - - fromAddress="0x882F5a2c1C429e6592D801486566D0753BC1dD04" - toAddress="0x4FC29eDF46859A67c5Bfa894C77a4E3C69353202" - fromTokenSymbol="ETH" - fromValue="0x1b667a56d488000" - fromValueFormatted="0.1234" - */ - return .init( - transaction: transaction, - namedFromAddress: NamedAddresses.name(for: transaction.fromAddress, accounts: accountInfos), - fromAddress: transaction.fromAddress, - namedToAddress: NamedAddresses.name(for: transaction.ethTxToAddress, accounts: accountInfos), - toAddress: transaction.ethTxToAddress, - networkSymbol: network.symbol, - details: .ethSend( - .init( - fromToken: network.nativeToken, - fromValue: fromValue, - fromAmount: fromValueFormatted, - fromFiat: fromFiat, - gasFee: gasFee( - from: transaction, - network: network, - assetRatios: assetRatios, - currencyFormatter: currencyFormatter + if let filTxData = transaction.txDataUnion.filTxData { // FIL send tx + let sendValue = filTxData.value + var sendValueFormatted = "" + var sendFiat = "$0.00" + sendValueFormatted = formatter.decimalString(for: sendValue, radix: .decimal, decimals: Int(network.nativeToken.decimals))?.trimmingTrailingZeros ?? "" + sendFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[network.nativeToken.assetRatioId.lowercased(), default: 0] * (Double(sendValueFormatted) ?? 0))) ?? "$0.00" + let gasLimitValueFormatted = formatter.decimalString(for: filTxData.gasLimit, radix: .decimal, decimals: Int(network.nativeToken.decimals))?.trimmingTrailingZeros ?? "" + let gasPremiumValueFormatted = formatter.decimalString(for: filTxData.gasPremium, radix: .decimal, decimals: Int(network.nativeToken.decimals))?.trimmingTrailingZeros ?? "" + let gasFeeCapValueFormatted = formatter.decimalString(for: filTxData.gasFeeCap, radix: .decimal, decimals: Int(network.nativeToken.decimals))?.trimmingTrailingZeros ?? "" + /* Example + Send 1 FIL + + fromAddress="t1xqhfiydm2yq6augugonr4zpdllh77iw53aesdes" + toAddress="t895quq7gkjh6ebshr7qi2ud7vycel4m7x6dvsekf" + sendTokenSymbol="FL" + sendValue="1000000000000000000" + sendValueFormatted="1" + gasPremiumValue="100911" + gasLimitValue="1527953" + gasFeeCapValue="101965" + gasPremiumValueFormatted="0.000000000000100911" + gasLimitValueFormatted="0.000000000001527953" + gasFeeCapValueFormatted="0.000000000000101965" + */ + return .init( + transaction: transaction, + namedFromAddress: NamedAddresses.name(for: transaction.fromAddress, accounts: accountInfos), + fromAddress: transaction.fromAddress, + namedToAddress: NamedAddresses.name(for: filTxData.to, accounts: accountInfos), + toAddress: filTxData.to, + networkSymbol: network.symbol, + details: .filSend( + .init( + sendToken: network.nativeToken, + sendValue: filTxData.value, + sendAmount: sendValueFormatted, + sendFiat: sendFiat, + gasPremium: gasPremiumValueFormatted, + gasLimit: gasLimitValueFormatted, + gasFeeCap: gasFeeCapValueFormatted, + gasFee: gasFee( + from: transaction, + network: network, + assetRatios: assetRatios, + currencyFormatter: currencyFormatter + ) ) ) ) - ) + } else { + let fromValue = transaction.ethTxValue + let fromValueFormatted = formatter.decimalString(for: fromValue.removingHexPrefix, radix: .hex, decimals: Int(network.decimals))?.trimmingTrailingZeros ?? "" + let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[network.nativeToken.assetRatioId.lowercased(), default: 0] * (Double(fromValueFormatted) ?? 0))) ?? "$0.00" + /* Example: + Send 0.1234 ETH + + fromAddress="0x882F5a2c1C429e6592D801486566D0753BC1dD04" + toAddress="0x4FC29eDF46859A67c5Bfa894C77a4E3C69353202" + fromTokenSymbol="ETH" + fromValue="0x1b667a56d488000" + fromValueFormatted="0.1234" + */ + return .init( + transaction: transaction, + namedFromAddress: NamedAddresses.name(for: transaction.fromAddress, accounts: accountInfos), + fromAddress: transaction.fromAddress, + namedToAddress: NamedAddresses.name(for: transaction.ethTxToAddress, accounts: accountInfos), + toAddress: transaction.ethTxToAddress, + networkSymbol: network.symbol, + details: .ethSend( + .init( + fromToken: network.nativeToken, + fromValue: fromValue, + fromAmount: fromValueFormatted, + fromFiat: fromFiat, + gasFee: gasFee( + from: transaction, + network: network, + assetRatios: assetRatios, + currencyFormatter: currencyFormatter + ) + ) + ) + ) + } case .erc20Transfer: guard let toAddress = transaction.txArgs[safe: 0], let fromValue = transaction.txArgs[safe: 1], @@ -495,6 +569,8 @@ enum TransactionParser { ) case .erc1155SafeTransferFrom: return nil + case .ethFilForwarderTransfer: + return nil @unknown default: return nil } @@ -576,6 +652,7 @@ struct ParsedTransaction: Equatable { case solSplTokenTransfer(SendDetails) case solDappTransaction(SolanaTxDetails) case solSwapTransaction(SolanaTxDetails) + case filSend(FilSendDetails) case other } @@ -615,6 +692,8 @@ struct ParsedTransaction: Equatable { return details.gasFee case .erc721Transfer, .other: return nil + case let .filSend(details): + return details.gasFee } } @@ -750,6 +829,26 @@ struct SolanaTxDetails: Equatable { let instructions: [ParsedSolanaInstruction] } +struct FilSendDetails: Equatable { + /// Token being sent + let sendToken: BraveWallet.BlockchainToken? + /// send value prior to formatting + let sendValue: String + /// send amount formatted + let sendAmount: String + /// The amount formatted as currency + let sendFiat: String? + + /// Gas premium for the transaction + let gasPremium: String? + /// Gas limit for the transaction + let gasLimit: String? + /// Gas fee cap for the transaction + let gasFeeCap: String? + /// Gas fee for the transaction + let gasFee: GasFee? +} + extension BraveWallet.TransactionInfo { /// Use `TransactionParser` to build a `ParsedTransaction` model for this transaction. func parsedTransaction( diff --git a/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift b/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift index 61fb53c67ed..1e10a4c436c 100644 --- a/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift +++ b/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift @@ -16,8 +16,14 @@ extension BraveWallet.TransactionInfo { } } var isEIP1559Transaction: Bool { - guard let ethTxData1559 = txDataUnion.ethTxData1559 else { return false } - return !ethTxData1559.maxPriorityFeePerGas.isEmpty && !ethTxData1559.maxFeePerGas.isEmpty + if coin == .eth { + guard let ethTxData1559 = txDataUnion.ethTxData1559 else { return false } + return !ethTxData1559.maxPriorityFeePerGas.isEmpty && !ethTxData1559.maxFeePerGas.isEmpty + } else if coin == .fil { + guard let filTxData = txDataUnion.filTxData else { return false } + return !filTxData.gasPremium.isEmpty && !filTxData.gasFeeCap.isEmpty + } + return false } var ethTxToAddress: String { // Eth transaction are all coming as `ethTxData1559` @@ -104,18 +110,18 @@ extension BraveWallet.AccountId { } extension BraveWallet.CoinType { - public var keyringId: BraveWallet.KeyringId { + public var keyringIds: [BraveWallet.KeyringId] { switch self { case .eth: - return BraveWallet.KeyringId.default + return [.default] case .sol: - return BraveWallet.KeyringId.solana + return [.solana] case .fil: - return BraveWallet.KeyringId.filecoin + return [.filecoin, .filecoinTestnet] case .btc: - return BraveWallet.KeyringId.bitcoin84 + return [.bitcoin84, .bitcoin84Testnet] @unknown default: - return BraveWallet.KeyringId.default + return [.default] } } @@ -246,6 +252,18 @@ extension BraveWallet.NetworkInfo { var walletUserAssetGroupId: String { "\(coin.rawValue).\(chainId)" } + + /// Generate the link for a submitted transaction with given transaction hash and coin type. + func txBlockExplorerLink(txHash: String, for coin: BraveWallet.CoinType) -> URL? { + if coin != .fil, + let baseURL = blockExplorerUrls.first.map(URL.init(string:)) { + return baseURL?.appendingPathComponent("tx/\(txHash)") + } else if var urlComps = blockExplorerUrls.first.map(URLComponents.init(string:)) { + urlComps?.queryItems = [URLQueryItem(name: "cid", value: txHash)] + return urlComps?.url + } + return nil + } } extension BraveWallet.BlockchainToken { @@ -342,6 +360,23 @@ extension BraveWallet.CoinMarket { } } +extension BraveWallet.KeyringId { + static func keyringId(for coin: BraveWallet.CoinType, on chainId: String) -> BraveWallet.KeyringId { + switch coin { + case .eth: + return .default + case .sol: + return .solana + case .fil: + return chainId == BraveWallet.FilecoinMainnet ? .filecoin : .filecoinTestnet + case.btc: + return chainId == BraveWallet.BitcoinMainnet ? .bitcoin84 : .bitcoin84Testnet + @unknown default: + return .default + } + } +} + public extension String { /// Returns true if the string ends with a supported ENS extension. var endsWithSupportedENSExtension: Bool { diff --git a/Sources/BraveWallet/Extensions/BraveWalletSwiftUIExtensions.swift b/Sources/BraveWallet/Extensions/BraveWalletSwiftUIExtensions.swift index 058d2360c5c..565ea10c6d7 100644 --- a/Sources/BraveWallet/Extensions/BraveWalletSwiftUIExtensions.swift +++ b/Sources/BraveWallet/Extensions/BraveWalletSwiftUIExtensions.swift @@ -23,6 +23,10 @@ extension BraveWallet.AccountInfo: Identifiable { public var coin: BraveWallet.CoinType { accountId.coin } + + public var keyringId: BraveWallet.KeyringId { + accountId.keyringId + } } extension BraveWallet.TransactionInfo: Identifiable { diff --git a/Sources/BraveWallet/Extensions/KeyringServiceExtensions.swift b/Sources/BraveWallet/Extensions/KeyringServiceExtensions.swift index a5b75134152..df340c6332b 100644 --- a/Sources/BraveWallet/Extensions/KeyringServiceExtensions.swift +++ b/Sources/BraveWallet/Extensions/KeyringServiceExtensions.swift @@ -18,9 +18,39 @@ extension BraveWalletKeyringService { of: BraveWallet.KeyringInfo.self, returning: [BraveWallet.KeyringInfo].self, body: { @MainActor group in - for coin in coins { + let keyringIds: [BraveWallet.KeyringId] = coins.flatMap(\.keyringIds) + for keyringId in keyringIds { group.addTask { @MainActor in - await self.keyringInfo(coin.keyringId) + await self.keyringInfo(keyringId) + } + } + return await group.reduce([BraveWallet.KeyringInfo](), { partialResult, prior in + return partialResult + [prior] + }) + .sorted(by: { lhs, rhs in + if lhs.coin == .fil && rhs.coin == .fil { + return lhs.id == BraveWallet.KeyringId.filecoin + } else { + return (lhs.coin ?? .eth).sortOrder < (rhs.coin ?? .eth).sortOrder + } + }) + } + ) + return allKeyrings + } + + // Fetches all keyrings for all given keyring IDs + func keyrings( + for keyringIds: [BraveWallet.KeyringId] + ) async -> [BraveWallet.KeyringInfo] { + var allKeyrings: [BraveWallet.KeyringInfo] = [] + allKeyrings = await withTaskGroup( + of: BraveWallet.KeyringInfo.self, + returning: [BraveWallet.KeyringInfo].self, + body: { @MainActor group in + for keyringId in keyringIds { + group.addTask { @MainActor in + await self.keyringInfo(keyringId) } } return await group.reduce([BraveWallet.KeyringInfo](), { partialResult, prior in diff --git a/Sources/BraveWallet/Extensions/RpcServiceExtensions.swift b/Sources/BraveWallet/Extensions/RpcServiceExtensions.swift index 056c753ccbd..fbe61e4bbb2 100644 --- a/Sources/BraveWallet/Extensions/RpcServiceExtensions.swift +++ b/Sources/BraveWallet/Extensions/RpcServiceExtensions.swift @@ -84,7 +84,24 @@ extension BraveWalletJsonRpcService { } } } - case .fil, .btc: + case .fil: + balance(account.address, coin: account.coin, chainId: network.chainId) { amount, status, _ in + guard status == .success && !amount.isEmpty else { + completion(nil) + return + } + let formatter = WeiFormatter(decimalFormatStyle: decimalFormatStyle) + if let valueString = formatter.decimalString( + for: "\(amount)", + radix: .decimal, + decimals: Int(token.decimals) + ) { + completion(Double(valueString)) + } else { + completion(nil) + } + } + case .btc: completion(nil) @unknown default: completion(nil) @@ -182,7 +199,24 @@ extension BraveWalletJsonRpcService { } } } - case .fil, .btc: + case .fil: + balance(accountAddress, coin: token.coin, chainId: network.chainId) { amount, status, _ in + guard status == .success && !amount.isEmpty else { + completion(nil) + return + } + let formatter = WeiFormatter(decimalFormatStyle: decimalFormatStyle) + if let valueString = formatter.decimalString( + for: "\(amount)", + radix: .decimal, + decimals: Int(token.decimals) + ) { + completion(BDouble(valueString)) + } else { + completion(nil) + } + } + case .btc: completion(nil) @unknown default: completion(nil) @@ -261,9 +295,15 @@ extension BraveWalletJsonRpcService { /// Returns an array of all networks for the supported coin types. Result will exclude test networks if test networks is set to /// not shown in Wallet Settings @MainActor func allNetworksForSupportedCoins(respectTestnetPreference: Bool = true) async -> [BraveWallet.NetworkInfo] { + await allNetworks(for: WalletConstants.supportedCoinTypes().elements, respectTestnetPreference: respectTestnetPreference) + } + + /// Returns an array of all networks for givin coins. Result will exclude test networks if test networks is set to + /// not shown in Wallet Settings + @MainActor func allNetworks(for coins: [BraveWallet.CoinType], respectTestnetPreference: Bool = true) async -> [BraveWallet.NetworkInfo] { await withTaskGroup(of: [BraveWallet.NetworkInfo].self) { @MainActor [weak self] group -> [BraveWallet.NetworkInfo] in guard let self = self else { return [] } - for coinType in WalletConstants.supportedCoinTypes { + for coinType in coins { group.addTask { @MainActor in let chains = await self.allNetworks(coinType) return chains.filter { // localhost not supported diff --git a/Sources/BraveWallet/Extensions/WalletTxServiceExtensions.swift b/Sources/BraveWallet/Extensions/WalletTxServiceExtensions.swift index 21c64142c3e..7fb8f82344d 100644 --- a/Sources/BraveWallet/Extensions/WalletTxServiceExtensions.swift +++ b/Sources/BraveWallet/Extensions/WalletTxServiceExtensions.swift @@ -10,16 +10,16 @@ extension BraveWalletTxService { // Fetches all pending transactions for all given keyrings func pendingTransactions( - chainIdsForCoin: [BraveWallet.CoinType: [String]], + networksForCoin: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]], for keyrings: [BraveWallet.KeyringInfo] ) async -> [BraveWallet.TransactionInfo] { - await allTransactions(chainIdsForCoin: chainIdsForCoin, for: keyrings) + await allTransactions(networksForCoin: networksForCoin, for: keyrings) .filter { $0.txStatus == .unapproved } } // Fetches all transactions for all given keyrings func allTransactions( - chainIdsForCoin: [BraveWallet.CoinType: [String]], + networksForCoin: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]], for keyrings: [BraveWallet.KeyringInfo] ) async -> [BraveWallet.TransactionInfo] { return await withTaskGroup( @@ -27,13 +27,13 @@ extension BraveWalletTxService { body: { @MainActor group in for keyring in keyrings { guard let keyringCoin = keyring.coin, - let chainIdsForKeyringCoin = chainIdsForCoin[keyringCoin] else { + let networksForKeyringCoin = networksForCoin[keyringCoin] else { continue } - for chainId in chainIdsForKeyringCoin { - for info in keyring.accountInfos { + for info in keyring.accountInfos { + for network in networksForKeyringCoin where network.supportedKeyrings.contains(keyring.id.rawValue as NSNumber) { group.addTask { @MainActor in - await self.allTransactionInfo(info.coin, chainId: chainId, from: info.address) + await self.allTransactionInfo(info.coin, chainId: network.chainId, from: info.address) } } } @@ -55,7 +55,7 @@ extension BraveWalletTxService { return await withTaskGroup( of: [BraveWallet.TransactionInfo].self, body: { @MainActor group in - for network in networks { + for network in networks where network.supportedKeyrings.contains(accountInfo.accountId.keyringId.rawValue as NSNumber) { group.addTask { @MainActor in await self.allTransactionInfo(accountInfo.coin, chainId: network.chainId, from: accountInfo.address) } diff --git a/Sources/BraveWallet/Panels/Connect/EditSiteConnectionView.swift b/Sources/BraveWallet/Panels/Connect/EditSiteConnectionView.swift index cf72c0ba5e1..1065fc81d7d 100644 --- a/Sources/BraveWallet/Panels/Connect/EditSiteConnectionView.swift +++ b/Sources/BraveWallet/Panels/Connect/EditSiteConnectionView.swift @@ -205,8 +205,8 @@ struct EditSiteConnectionView_Previews: PreviewProvider { EditSiteConnectionView( keyringStore: { let store = KeyringStore.previewStoreWithWalletCreated - store.addPrimaryAccount("Account 2", coin: .eth, completion: nil) - store.addPrimaryAccount("Account 3", coin: .eth, completion: nil) + store.addPrimaryAccount("Account 2", coin: .eth, chainId: BraveWallet.MainnetChainId, completion: nil) + store.addPrimaryAccount("Account 3", coin: .eth, chainId: BraveWallet.MainnetChainId, completion: nil) return store }(), origin: .init(url: URL(string: "https://app.uniswap.org")!), diff --git a/Sources/BraveWallet/Panels/Connect/NewSiteConnectionView.swift b/Sources/BraveWallet/Panels/Connect/NewSiteConnectionView.swift index 8d1485f61e9..6698bff5ac6 100644 --- a/Sources/BraveWallet/Panels/Connect/NewSiteConnectionView.swift +++ b/Sources/BraveWallet/Panels/Connect/NewSiteConnectionView.swift @@ -256,8 +256,8 @@ struct NewSiteConnectionView_Previews: PreviewProvider { coin: .eth, keyringStore: { let store = KeyringStore.previewStoreWithWalletCreated - store.addPrimaryAccount("Account 2", coin: .eth, completion: nil) - store.addPrimaryAccount("Account 3", coin: .eth, completion: nil) + store.addPrimaryAccount("Account 2", coin: .eth, chainId: BraveWallet.MainnetChainId, completion: nil) + store.addPrimaryAccount("Account 3", coin: .eth, chainId: BraveWallet.MainnetChainId, completion: nil) return store }(), onConnect: { _ in }, diff --git a/Sources/BraveWallet/Panels/WalletPanelView.swift b/Sources/BraveWallet/Panels/WalletPanelView.swift index 5c4737af4af..b70e9f462fc 100644 --- a/Sources/BraveWallet/Panels/WalletPanelView.swift +++ b/Sources/BraveWallet/Panels/WalletPanelView.swift @@ -355,6 +355,8 @@ struct WalletPanelView: View { } } return true + } else if account.coin == .fil { + return true } else { return false } @@ -518,7 +520,7 @@ struct WalletPanelView: View { } } .onAppear { - if let accountCreationRequest = WalletProviderAccountCreationRequestManager.shared.firstPendingRequest(for: origin, coinTypes: Array(WalletConstants.supportedCoinTypes)) { + if let accountCreationRequest = WalletProviderAccountCreationRequestManager.shared.firstPendingRequest(for: origin, coinTypes: WalletConstants.supportedCoinTypes(.dapps).elements) { presentWalletWithContext(.createAccount(accountCreationRequest)) } else if let request = WalletProviderPermissionRequestsManager.shared.firstPendingRequest(for: origin, coinTypes: [.eth, .sol]) { presentWalletWithContext(.requestPermissions(request, onPermittedAccountsUpdated: { accounts in diff --git a/Sources/BraveWallet/Preview Content/MockContent.swift b/Sources/BraveWallet/Preview Content/MockContent.swift index a5d8e78f7c1..e9cf651373c 100644 --- a/Sources/BraveWallet/Preview Content/MockContent.swift +++ b/Sources/BraveWallet/Preview Content/MockContent.swift @@ -134,6 +134,24 @@ extension BraveWallet.BlockchainToken { chainId: BraveWallet.SolanaMainnet, coin: .sol ) + + static let mockFilToken: BraveWallet.BlockchainToken = .init( + contractAddress: "", + name: "Filcoin", + logo: "", + isErc20: false, + isErc721: false, + isErc1155: false, + isNft: false, + isSpam: false, + symbol: "FIL", + decimals: 18, + visible: false, + tokenId: "", + coingeckoId: "", + chainId: BraveWallet.FilecoinMainnet, + coin: .fil + ) } extension BraveWallet.AccountInfo { @@ -348,6 +366,36 @@ extension BraveWallet.TransactionInfo { effectiveRecipient: nil // Currently only available for ETH and FIL ) } + /// Filecoin Unapproved Send + static let mockFilUnapprovedSend = BraveWallet.TransactionInfo( + id: UUID().uuidString, + fromAddress: "t165quq7gkjh6ebshr7qi2ud7vycel4m7x6dvfvgb", + from: BraveWallet.AccountInfo.mockFilAccount.accountId, + txHash: "", + txDataUnion: + .init(filTxData: + .init(nonce: "", + gasPremium: "100911", + gasFeeCap: "101965", + gasLimit: "1527953", + maxFee: "0", + to: "t1xqhfiydm2yq6augugonr4zpdllh77iw53aexztb", + value: "1000000000000000000" + ) + ), + txStatus: .unapproved, + txType: .other, + txParams: [], + txArgs: [ + ], + createdTime: Date(timeIntervalSince1970: 1636399671), // Monday, November 8, 2021 7:27:51 PM + submittedTime: Date(timeIntervalSince1970: 1636399673), // Monday, November 8, 2021 7:27:53 PM + confirmedTime: Date(timeIntervalSince1970: 1636402508), // Monday, November 8, 2021 8:15:08 PM + originInfo: nil, + groupId: nil, + chainId: BraveWallet.FilecoinMainnet, + effectiveRecipient: nil + ) static private func _transactionBase64ToData(_ base64String: String) -> [NSNumber] { guard let data = Data(base64Encoded: base64String) else { return [] } return Array(data).map(NSNumber.init(value:)) diff --git a/Sources/BraveWallet/Preview Content/MockJsonRpcService.swift b/Sources/BraveWallet/Preview Content/MockJsonRpcService.swift index 3c6e9420476..552d725b723 100644 --- a/Sources/BraveWallet/Preview Content/MockJsonRpcService.swift +++ b/Sources/BraveWallet/Preview Content/MockJsonRpcService.swift @@ -358,6 +358,34 @@ extension BraveWallet.NetworkInfo { supportedKeyrings: [BraveWallet.KeyringId.solana.rawValue].map(NSNumber.init(value:)), isEip1559: false ) + static let mockFilecoinMainnet: BraveWallet.NetworkInfo = .init( + chainId: BraveWallet.FilecoinMainnet, + chainName: "Filecoin Mainnet", + blockExplorerUrls: [""], + iconUrls: [], + activeRpcEndpointIndex: 0, + rpcEndpoints: [URL(string: "https://rpc.ankr.com/filecoin")!], + symbol: "FIL", + symbolName: "Filecoin", + decimals: 18, + coin: .fil, + supportedKeyrings: [BraveWallet.KeyringId.filecoin.rawValue].map(NSNumber.init(value:)), + isEip1559: true + ) + static let mockFilecoinTestnet: BraveWallet.NetworkInfo = .init( + chainId: BraveWallet.FilecoinTestnet, + chainName: "Filecoin Testnet", + blockExplorerUrls: [""], + iconUrls: [], + activeRpcEndpointIndex: 0, + rpcEndpoints: [URL(string: "https://rpc.ankr.com/filecoin_testnet")!], + symbol: "FIL", + symbolName: "Filecoin", + decimals: 18, + coin: .fil, + supportedKeyrings: [BraveWallet.KeyringId.filecoinTestnet.rawValue].map(NSNumber.init(value:)), + isEip1559: true + ) } #endif diff --git a/Sources/BraveWallet/Preview Content/MockKeyringService.swift b/Sources/BraveWallet/Preview Content/MockKeyringService.swift index a48a4bb75fe..1677758a7e3 100644 --- a/Sources/BraveWallet/Preview Content/MockKeyringService.swift +++ b/Sources/BraveWallet/Preview Content/MockKeyringService.swift @@ -397,6 +397,20 @@ extension BraveWallet.AccountInfo { name: "Filecoin Account 1", hardware: nil ) + + static let mockFilTestnetAccount: BraveWallet.AccountInfo = .init( + accountId: .init( + coin: .fil, + keyringId: BraveWallet.KeyringId.filecoinTestnet, + kind: .derived, + address: "mock_fil_testnet_id", + bitcoinAccountIndex: 0, + uniqueKey: "mock_fil_testnet_id" + ), + address: "mock_fil_testnet_id", + name: "Filecoin Testnet 1", + hardware: nil + ) } extension BraveWallet.KeyringInfo { @@ -423,4 +437,12 @@ extension BraveWallet.KeyringInfo { isBackedUp: false, accountInfos: [.mockFilAccount] ) + + static let mockFilecoinTestnetKeyringInfo: BraveWallet.KeyringInfo = .init( + id: BraveWallet.KeyringId.filecoinTestnet, + isKeyringCreated: true, + isLocked: false, + isBackedUp: false, + accountInfos: [.mockFilTestnetAccount] + ) } diff --git a/Sources/BraveWallet/Settings/Web3SettingsView.swift b/Sources/BraveWallet/Settings/Web3SettingsView.swift index 00f2fbe7d32..caf0ee7bf5d 100644 --- a/Sources/BraveWallet/Settings/Web3SettingsView.swift +++ b/Sources/BraveWallet/Settings/Web3SettingsView.swift @@ -253,7 +253,7 @@ private struct WalletSettingsView: View { .foregroundColor(Color(.secondaryBraveLabel)) ) { Group { - ForEach(Array(WalletConstants.supportedCoinTypes)) { coin in + ForEach(WalletConstants.supportedCoinTypes(.dapps)) { coin in NavigationLink( destination: DappsSettings( diff --git a/Sources/BraveWallet/WalletConstants.swift b/Sources/BraveWallet/WalletConstants.swift index 7d30e4ae0c4..34990236405 100644 --- a/Sources/BraveWallet/WalletConstants.swift +++ b/Sources/BraveWallet/WalletConstants.swift @@ -75,10 +75,20 @@ public struct WalletConstants { BraveWallet.MainnetChainId, BraveWallet.FilecoinMainnet ] + + public enum SupportedCoinTypesMode { + case general + case dapps + } - /// The currently supported coin types. - public static var supportedCoinTypes: OrderedSet { - return [.eth, .sol] + /// The currently supported coin types in wallet + public static func supportedCoinTypes(_ mode: SupportedCoinTypesMode = .general) -> OrderedSet { + switch mode { + case .general: + return [.eth, .sol, .fil] + case .dapps: + return [.eth, .sol] + } } /// The supported Ethereum Name Service (ENS) extensions diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index a8728329179..f1e74c09454 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -1976,6 +1976,13 @@ extension Strings { value: "Domain doesn\'t have a linked %@ address", comment: "An error that appears below the send crypto address text field, when the input `To` domain/url that we cannot resolve to a wallet address. The '%@' will be replaced with the coin type Ex. `Domain doesn\'t have a linked ETH address`" ) + public static let sendErrorInvalidRecipientAddress = NSLocalizedString( + "wallet.sendErrorInvalidRecipientAddress", + tableName: "BraveWallet", + bundle: .module, + value: "Invalid recipient address", + comment: "An error that appears below the send crypto address text field, when the input `To` Filecoin address that is invalid" + ) public static let customNetworkChainIdTitle = NSLocalizedString( "wallet.customNetworkChainIdTitle", tableName: "BraveWallet", diff --git a/Sources/BraveWallet/WalletUserAssetManager.swift b/Sources/BraveWallet/WalletUserAssetManager.swift index c6e28c742b1..86af0533ea3 100644 --- a/Sources/BraveWallet/WalletUserAssetManager.swift +++ b/Sources/BraveWallet/WalletUserAssetManager.swift @@ -7,6 +7,7 @@ import Foundation import Data import BraveCore import Preferences +import CoreData public protocol WalletUserAssetManagerType: AnyObject { func getAllVisibleAssetsInNetworkAssets(networks: [BraveWallet.NetworkInfo]) -> [NetworkAssets] @@ -87,19 +88,39 @@ public class WalletUserAssetManager: WalletUserAssetManagerType { WalletUserAssetGroup.removeGroup(groupId, completion: completion) } - public func migrateUserAssets(for coin: BraveWallet.CoinType? = nil, completion: (() -> Void)? = nil) { - guard !Preferences.Wallet.migrateCoreToWalletUserAssetCompleted.value else { - completion?() - return - } + public func migrateUserAssets(completion: (() -> Void)? = nil) { Task { @MainActor in - var fetchedUserAssets: [String: [BraveWallet.BlockchainToken]] = [:] - var networks: [BraveWallet.NetworkInfo] = [] - if let coin = coin { - networks = await rpcService.allNetworks(coin) + if !Preferences.Wallet.migrateCoreToWalletUserAssetCompleted.value { + migrateUserAssets(for: Array(WalletConstants.supportedCoinTypes()), completion: completion) } else { - networks = await rpcService.allNetworksForSupportedCoins(respectTestnetPreference: false) + let allNetworks = await rpcService.allNetworksForSupportedCoins(respectTestnetPreference: false) + DataController.performOnMainContext { context in + let newCoins = self.allNewCoinsIntroduced(networks: allNetworks, context: context) + if !newCoins.isEmpty { + self.migrateUserAssets(for: newCoins, completion: completion) + } else { + completion?() + } + } } + } + } + + private func allNewCoinsIntroduced(networks: [BraveWallet.NetworkInfo], context: NSManagedObjectContext) -> [BraveWallet.CoinType] { + guard let assetGroupIds = WalletUserAssetGroup.getAllGroups(context: context)?.map({ group in + group.groupId + }) else { return WalletConstants.supportedCoinTypes().elements } + var newCoins: Set = [] + for network in networks where !assetGroupIds.contains("\(network.coin.rawValue).\(network.chainId)") { + newCoins.insert(network.coin) + } + return Array(newCoins) + } + + private func migrateUserAssets(for coins: [BraveWallet.CoinType], completion: (() -> Void)?) { + Task { @MainActor in + var fetchedUserAssets: [String: [BraveWallet.BlockchainToken]] = [:] + let networks: [BraveWallet.NetworkInfo] = await rpcService.allNetworks(for: coins, respectTestnetPreference: false) let networkAssets = await walletService.allUserAssets(in: networks) for networkAsset in networkAssets { fetchedUserAssets["\(networkAsset.network.coin.rawValue).\(networkAsset.network.chainId)"] = networkAsset.tokens diff --git a/Tests/BraveWalletTests/AccountActivityStoreTests.swift b/Tests/BraveWalletTests/AccountActivityStoreTests.swift index 8d1823735b7..52fc6f1f189 100644 --- a/Tests/BraveWalletTests/AccountActivityStoreTests.swift +++ b/Tests/BraveWalletTests/AccountActivityStoreTests.swift @@ -14,7 +14,8 @@ class AccountActivityStoreTests: XCTestCase { let networks: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ .eth: [.mockMainnet, .mockGoerli], - .sol: [.mockSolana, .mockSolanaTestnet] + .sol: [.mockSolana, .mockSolanaTestnet], + .fil: [.mockFilecoinMainnet, .mockFilecoinTestnet] ] let tokenRegistry: [BraveWallet.CoinType: [BraveWallet.BlockchainToken]] = [:] let mockAssetPrices: [BraveWallet.AssetPrice] = [ @@ -23,7 +24,9 @@ class AccountActivityStoreTests: XCTestCase { toAsset: "usd", price: "1.00", assetTimeframeChange: "-57.23"), .init(fromAsset: "sol", toAsset: "usd", price: "2.00", assetTimeframeChange: "-57.23"), .init(fromAsset: BraveWallet.BlockchainToken.mockSpdToken.assetRatioId.lowercased(), - toAsset: "usd", price: "0.50", assetTimeframeChange: "-57.23") + toAsset: "usd", price: "0.50", assetTimeframeChange: "-57.23"), + .init(fromAsset: BraveWallet.BlockchainToken.mockFilToken.assetRatioId.lowercased(), + toAsset: "usd", price: "4.00", assetTimeframeChange: "-57.23") ] private func setupServices( @@ -31,13 +34,26 @@ class AccountActivityStoreTests: XCTestCase { mockERC20BalanceWei: String = "", mockERC721BalanceWei: String = "", mockLamportBalance: UInt64 = 0, - mockSplTokenBalances: [String: String] = [:], // [tokenMintAddress: balance] + mockSplTokenBalances: [String: String] = [:], // [tokenMintAddress: balance], + mockFilBalance: String = "", + mockFilTestnetBalance: String = "", transactions: [BraveWallet.TransactionInfo] ) -> (BraveWallet.TestKeyringService, BraveWallet.TestJsonRpcService, BraveWallet.TestBraveWalletService, BraveWallet.TestBlockchainRegistry, BraveWallet.TestAssetRatioService, BraveWallet.TestTxService, BraveWallet.TestSolanaTxManagerProxy, IpfsAPI) { let keyringService = BraveWallet.TestKeyringService() keyringService._addObserver = { _ in } - keyringService._keyringInfo = { _, completion in - completion(.mockDefaultKeyringInfo) + keyringService._keyringInfo = { keyringInfo, completion in + switch keyringInfo { + case .default: + completion(.mockDefaultKeyringInfo) + case .solana: + completion(.mockSolanaKeyringInfo) + case .filecoin: + completion(.mockFilecoinKeyringInfo) + case .filecoinTestnet: + completion(.mockFilecoinTestnetKeyringInfo) + default: + completion(.mockDefaultKeyringInfo) + } } let rpcService = BraveWallet.TestJsonRpcService() @@ -45,8 +61,12 @@ class AccountActivityStoreTests: XCTestCase { rpcService._allNetworks = { coin, completion in completion(self.networks[coin] ?? []) } - rpcService._balance = { _, _, _, completion in - completion(mockEthBalanceWei, .success, "") // eth balance + rpcService._balance = { _, coin, chainId, completion in + if coin == .eth { + completion(mockEthBalanceWei, .success, "") // eth balance + } else { // .fil + completion(chainId == BraveWallet.FilecoinMainnet ? mockFilBalance : mockFilTestnetBalance, .success, "") + } } rpcService._erc20TokenBalance = { _, _, _, completion in completion(mockERC20BalanceWei, .success, "") @@ -111,7 +131,7 @@ class AccountActivityStoreTests: XCTestCase { } func testUpdateEthereumAccount() { - let firstTransactionDate = Date(timeIntervalSince1970: 1636399671) // Monday, November 8, 2021 7:27:51 PM + let firstTransactionDate = Date(timeIntervalSince1970: 1636399671) // Monday, November 8, 2021 7:27:51 PM let account: BraveWallet.AccountInfo = .mockEthAccount let formatter = WeiFormatter(decimalFormatStyle: .decimals(precision: 18)) let mockEthDecimalBalance: Double = 0.0896 @@ -365,4 +385,135 @@ class AccountActivityStoreTests: XCTestCase { XCTAssertNil(error) } } + + func testUpdateFilecoinAccount() { + let firstTransactionDate = Date(timeIntervalSince1970: 1636399671) // Monday, November 8, 2021 7:27:51 PM + let account: BraveWallet.AccountInfo = .mockFilAccount + + let transactionData: BraveWallet.FilTxData = .init( + nonce: "", + gasPremium: "100911", + gasFeeCap: "101965", + gasLimit: "1527953", + maxFee: "0", + to: "t1xqhfiydm2yq6augugonr4zpdllh77iw53aexztb", + value: "1000000000000000000" + ) + let transaction = BraveWallet.TransactionInfo( + id: UUID().uuidString, + fromAddress: "t165quq7gkjh6ebshr7qi2ud7vycel4m7x6dvfvgb", + from: account.accountId, + txHash: "0xaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffggggg1234", + txDataUnion: .init(filTxData: transactionData), + txStatus: .unapproved, + txType: .other, + txParams: [], + txArgs: [ + ], + createdTime: firstTransactionDate, + submittedTime: Date(), + confirmedTime: Date(), + originInfo: nil, + groupId: nil, + chainId: BraveWallet.FilecoinMainnet, + effectiveRecipient: nil + ) + + let transactionCopy = transaction.copy() as! BraveWallet.TransactionInfo + transactionCopy.id = UUID().uuidString + transactionCopy.chainId = BraveWallet.FilecoinTestnet + + let formatter = WeiFormatter(decimalFormatStyle: .decimals(precision: 18)) + let mockFilDecimalBalance: Double = 1 + let filecoinMainnetDecimals = Int(BraveWallet.NetworkInfo.mockFilecoinMainnet.decimals) + let mockFilDecimalBalanceInWei = formatter.weiString(from: "\(mockFilDecimalBalance)", radix: .decimal, decimals: filecoinMainnetDecimals) ?? "" + let mockFileTestnetDecimalBalance: Double = 2 + let filecoinTestnetDecimals = Int(BraveWallet.NetworkInfo.mockFilecoinTestnet.decimals) + let mockFilTestnetDecimalBalanceInWei = formatter.weiString(from: "\(mockFileTestnetDecimalBalance)", radix: .decimal, decimals: filecoinTestnetDecimals) ?? "" + + let (keyringService, rpcService, walletService, blockchainRegistry, assetRatioService, txService, solTxManagerProxy, ipfsApi) = setupServices( + mockFilBalance: mockFilDecimalBalanceInWei, + mockFilTestnetBalance: mockFilTestnetDecimalBalanceInWei, + transactions: [transaction, transactionCopy].enumerated().map { (index, tx) in + // transactions sorted by created time, make sure they are in-order + tx.createdTime = firstTransactionDate.addingTimeInterval(TimeInterval(index * 10)) + return tx + } + ) + + let mockAssetManager = TestableWalletUserAssetManager() + mockAssetManager._getAllVisibleAssetsInNetworkAssets = { _ in + [ + NetworkAssets( + network: .mockFilecoinMainnet, + tokens: [ + BraveWallet.NetworkInfo.mockFilecoinMainnet.nativeToken.copy(asVisibleAsset: true) + ], + sortOrder: 0), + NetworkAssets( + network: .mockFilecoinTestnet, + tokens: [ + BraveWallet.NetworkInfo.mockFilecoinTestnet.nativeToken.copy(asVisibleAsset: true) + ], + sortOrder: 1) + ] + } + + let accountActivityStore = AccountActivityStore( + account: account, + observeAccountUpdates: false, + keyringService: keyringService, + walletService: walletService, + rpcService: rpcService, + assetRatioService: assetRatioService, + txService: txService, + blockchainRegistry: blockchainRegistry, + solTxManagerProxy: solTxManagerProxy, + ipfsApi: ipfsApi, + userAssetManager: mockAssetManager + ) + + let userVisibleAssetsExpectation = expectation(description: "accountActivityStore-assetStores") + accountActivityStore.$userVisibleAssets + .dropFirst() + .collect(2) + .sink { userVisibleAssets in + defer { userVisibleAssetsExpectation.fulfill() } + XCTAssertEqual(userVisibleAssets.count, 2) // empty assets, populated assets + guard let lastUpdatedVisibleAssets = userVisibleAssets.last else { + XCTFail("Unexpected test result") + return + } + XCTAssertEqual(lastUpdatedVisibleAssets.count, 2) + + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 0]?.token.symbol, BraveWallet.NetworkInfo.mockFilecoinMainnet.nativeToken.symbol) + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 0]?.network, BraveWallet.NetworkInfo.mockFilecoinMainnet) + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 0]?.totalBalance, mockFilDecimalBalance) + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 0]?.price, self.mockAssetPrices[safe: 4]?.price ?? "") + + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 1]?.token.symbol, BraveWallet.NetworkInfo.mockFilecoinTestnet.nativeToken.symbol) + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 1]?.network, BraveWallet.NetworkInfo.mockFilecoinTestnet) + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 1]?.totalBalance, mockFileTestnetDecimalBalance) + XCTAssertEqual(lastUpdatedVisibleAssets[safe: 1]?.price, self.mockAssetPrices[safe: 4]?.price ?? "") + } + .store(in: &cancellables) + + let transactionSummariesExpectation = expectation(description: "accountActivityStore-transactions") + XCTAssertTrue(accountActivityStore.transactionSummaries.isEmpty) + accountActivityStore.$transactionSummaries + .dropFirst() + .sink { transactionSummaries in + defer { transactionSummariesExpectation.fulfill() } + // summaries are tested in `TransactionParserTests`, just verify they are populated with correct tx + XCTAssertEqual(transactionSummaries.count, 1) // // should not have `transactionCopy` since it's on testnet but the account is on mainnet + XCTAssertEqual(transactionSummaries[safe: 0]?.txInfo, transaction) + XCTAssertEqual(transactionSummaries[safe: 0]?.txInfo.chainId, transaction.chainId) + }.store(in: &cancellables) + + accountActivityStore.update() + + waitForExpectations(timeout: 1) { error in + XCTAssertNil(error) + } + } } diff --git a/Tests/BraveWalletTests/AddressTests.swift b/Tests/BraveWalletTests/AddressTests.swift index 37bb7c48a19..8061c3f31df 100644 --- a/Tests/BraveWalletTests/AddressTests.swift +++ b/Tests/BraveWalletTests/AddressTests.swift @@ -86,4 +86,27 @@ class AddressTests: XCTestCase { XCTAssertNotEqual(address, result) XCTAssertEqual(zwspAddress, result) } + + func testIsFILAddress() { + let isMainnetAddressTrue = "f1ysqn2zflyeb1jqqi2bqbomgjtodunoplkfedbpa" + XCTAssertTrue(isMainnetAddressTrue.isFILAddress) + + let isTestnetAddressTrue = "t165quq7gkjh6ebshr7qi2ud7vycel4m7x6dvfvga" + XCTAssertTrue(isTestnetAddressTrue.isFILAddress) + + let isAddressFalseWrongPrefix = "a1ysqn2zflyeb1jqqi2bqbomgjtodunoplkfedbpa" + XCTAssertFalse(isAddressFalseWrongPrefix.isFILAddress) + + let isAddressFalseCorrentLength1 = "f1ysqn2zflyeb1jqqi2bqbomgjtodunoplkfedbpa" + XCTAssertTrue(isAddressFalseCorrentLength1.isFILAddress) + + let isAddressFalseCorrentLength2 = "f1ysqn2zflyeb1jqqi2bqbomgjtodunoplkfedbpa2ba" + XCTAssertTrue(isAddressFalseCorrentLength2.isFILAddress) + + let isAddressFalseCorrentLength3 = "f1ysqn2zflyeb1jqqi2bqbomgjtodunoplkfedbpaf1ysqn2zflyeb1jqqi2bqbomgjtodunoplkfedbpa1gdu" + XCTAssertTrue(isAddressFalseCorrentLength3.isFILAddress) + + let isAddressFalseWrongLength = "f1ysqn2zflyeb1jqqi2bqbomgjtodundbpa" + XCTAssertFalse(isAddressFalseWrongLength.isFILAddress) + } } diff --git a/Tests/BraveWalletTests/KeyringStoreTests.swift b/Tests/BraveWalletTests/KeyringStoreTests.swift index d483741da2c..6868f7accda 100644 --- a/Tests/BraveWalletTests/KeyringStoreTests.swift +++ b/Tests/BraveWalletTests/KeyringStoreTests.swift @@ -26,6 +26,8 @@ class KeyringStoreTests: XCTestCase { completion(.mockSolanaKeyringInfo) case BraveWallet.KeyringId.filecoin: completion(.mockFilecoinKeyringInfo) + case BraveWallet.KeyringId.filecoinTestnet: + completion(.mockFilecoinTestnetKeyringInfo) default: completion(.init()) } @@ -66,14 +68,14 @@ class KeyringStoreTests: XCTestCase { rpcService: rpcService ) - let expectedKeyrings = [BraveWallet.KeyringInfo.mockDefaultKeyringInfo, BraveWallet.KeyringInfo.mockSolanaKeyringInfo] + let expectedKeyrings = [BraveWallet.KeyringInfo.mockDefaultKeyringInfo, BraveWallet.KeyringInfo.mockSolanaKeyringInfo, BraveWallet.KeyringInfo.mockFilecoinKeyringInfo, BraveWallet.KeyringInfo.mockFilecoinTestnetKeyringInfo] let allTokensExpectation = expectation(description: "allKeyrings") store.$allKeyrings .dropFirst() .sink { allKeyrings in defer { allTokensExpectation.fulfill() } - XCTAssertEqual(allKeyrings.count, 2) + XCTAssertEqual(allKeyrings.count, 4) for keyring in allKeyrings { XCTAssertTrue(expectedKeyrings.contains(where: { $0.id == keyring.id })) } diff --git a/Tests/BraveWalletTests/NFTStoreTests.swift b/Tests/BraveWalletTests/NFTStoreTests.swift index bce57a3706d..9f0c5772de9 100644 --- a/Tests/BraveWalletTests/NFTStoreTests.swift +++ b/Tests/BraveWalletTests/NFTStoreTests.swift @@ -79,7 +79,7 @@ class NFTStoreTests: XCTestCase { case .sol: completion([solNetwork]) case .fil: - XCTFail("Should not fetch filecoin network") + completion([.mockFilecoinTestnet]) case .btc: XCTFail("Should not fetch btc network") @unknown default: diff --git a/Tests/BraveWalletTests/NetworkSelectionStoreTests.swift b/Tests/BraveWalletTests/NetworkSelectionStoreTests.swift index eb28e45d55f..d291a2fa5e1 100644 --- a/Tests/BraveWalletTests/NetworkSelectionStoreTests.swift +++ b/Tests/BraveWalletTests/NetworkSelectionStoreTests.swift @@ -15,7 +15,8 @@ import Preferences private let allNetworks: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ .eth: [.mockMainnet, .mockGoerli, .mockSepolia, .mockPolygon], - .sol: [.mockSolana, .mockSolanaTestnet] + .sol: [.mockSolana, .mockSolanaTestnet], + .fil: [.mockFilecoinTestnet] ] private func setupServices() -> (BraveWallet.TestKeyringService, BraveWallet.TestJsonRpcService, BraveWallet.TestBraveWalletService, BraveWallet.TestSwapService) { @@ -24,7 +25,7 @@ import Preferences let keyringService = BraveWallet.TestKeyringService() keyringService._keyringInfo = { keyringId, completion in - let isEthereumKeyringId = keyringId == BraveWallet.CoinType.eth.keyringId + let isEthereumKeyringId = keyringId == BraveWallet.KeyringId.default let keyring: BraveWallet.KeyringInfo = .init( id: BraveWallet.KeyringId.default, isKeyringCreated: true, @@ -218,11 +219,11 @@ import Preferences keyringService._keyringInfo = { keyringId, completion in let accountInfos: [BraveWallet.AccountInfo] switch keyringId { - case BraveWallet.CoinType.eth.keyringId: + case BraveWallet.KeyringId.default: accountInfos = accountInfosDict[.eth, default: []] - case BraveWallet.CoinType.sol.keyringId: + case BraveWallet.KeyringId.solana: accountInfos = accountInfosDict[.sol, default: []] - case BraveWallet.CoinType.fil.keyringId: + case BraveWallet.KeyringId.filecoin: accountInfos = accountInfosDict[.fil, default: []] default: accountInfos = [] @@ -272,5 +273,21 @@ import Preferences let didSwitchNetworks = await store.handleDismissAddAccount() XCTAssertTrue(didSwitchNetworks, "Expected to switch networks as an account was created") + + // create filecoin account + let selectFilecoinMainnetSuccess = await store.selectNetwork(.mockFilecoinMainnet) + XCTAssertFalse(selectFilecoinMainnetSuccess, "Expected failure to select network due to no accounts") + XCTAssertTrue(store.isPresentingNextNetworkAlert, "Expected to present next network alert") + + store.handleCreateAccountAlertResponse(shouldCreateAccount: true) + + XCTAssertFalse(store.isPresentingNextNetworkAlert, "Expected to set isPresentingNextNetworkAlert to false to hide alert") + XCTAssertTrue(store.isPresentingAddAccount, "Expected to set isPresentingAddAccount to true to present add network") + + // simulate an account created + accountInfosDict[.fil] = [.mockFilAccount] + + let didSwitchToFilecoinMainnet = await store.handleDismissAddAccount() + XCTAssertTrue(didSwitchToFilecoinMainnet, "Expected to switch networks as an account was created") } } diff --git a/Tests/BraveWalletTests/NetworkStoreTests.swift b/Tests/BraveWalletTests/NetworkStoreTests.swift index e4907d4c722..d16846dcb30 100644 --- a/Tests/BraveWalletTests/NetworkStoreTests.swift +++ b/Tests/BraveWalletTests/NetworkStoreTests.swift @@ -25,12 +25,13 @@ import Preferences let currentChainId = currentNetwork.chainId let allNetworks: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ .eth: [.mockMainnet, .mockGoerli, .mockSepolia, .mockPolygon, .mockCustomNetwork], - .sol: [.mockSolana, .mockSolanaTestnet] + .sol: [.mockSolana, .mockSolanaTestnet], + .fil: [.mockFilecoinMainnet, .mockFilecoinTestnet] ] let keyringService = BraveWallet.TestKeyringService() keyringService._keyringInfo = { keyringId, completion in - let isEthereumKeyringId = keyringId == BraveWallet.CoinType.eth.keyringId + let isEthereumKeyringId = keyringId == BraveWallet.KeyringId.default let keyring: BraveWallet.KeyringInfo = .init( id: BraveWallet.KeyringId.default, isKeyringCreated: true, @@ -151,6 +152,10 @@ import Preferences let error = await store.setSelectedChain(.mockSolana, isForOrigin: false) XCTAssertEqual(error, .selectedChainHasNoAccounts, "Expected chain has no accounts error") XCTAssertNotEqual(store.defaultSelectedChainId, BraveWallet.NetworkInfo.mockSolana.chainId) + + let selectFilecoinMainnetError = await store.setSelectedChain(.mockFilecoinMainnet, isForOrigin: false) + XCTAssertEqual(selectFilecoinMainnetError, .selectedChainHasNoAccounts, "Expected chain has no accounts error") + XCTAssertNotEqual(store.defaultSelectedChainId, BraveWallet.NetworkInfo.mockFilecoinMainnet.chainId) } func testUpdateChainList() async { @@ -171,7 +176,9 @@ import Preferences .mockGoerli, .mockSepolia, .mockPolygon, - .mockCustomNetwork + .mockCustomNetwork, + .mockFilecoinMainnet, + .mockFilecoinTestnet ] let expectedCustomChains: [BraveWallet.NetworkInfo] = [ @@ -184,7 +191,8 @@ import Preferences .dropFirst() .sink { allChains in defer { allChainsExpectation.fulfill() } - XCTAssertEqual(allChains, expectedAllChains) + XCTAssertEqual(allChains.count, expectedAllChains.count) + XCTAssertTrue(allChains.allSatisfy(expectedAllChains.contains(_:))) } .store(in: &cancellables) diff --git a/Tests/BraveWalletTests/PortfolioStoreTests.swift b/Tests/BraveWalletTests/PortfolioStoreTests.swift index 1f1199a61f7..4be18b49760 100644 --- a/Tests/BraveWalletTests/PortfolioStoreTests.swift +++ b/Tests/BraveWalletTests/PortfolioStoreTests.swift @@ -40,10 +40,19 @@ import Preferences $0.address = "mock_sol_id_2" $0.name = "Solana Account 2" } + let filAccount1: BraveWallet.AccountInfo = .mockFilAccount + let filAccount2 = (BraveWallet.AccountInfo.mockFilAccount.copy() as! BraveWallet.AccountInfo).then { + $0.address = "mock_fil_id_2" + $0.name = "Filecoin Account 2" + } + let filTestnetAccount: BraveWallet.AccountInfo = .mockFilTestnetAccount + // Networks let ethNetwork: BraveWallet.NetworkInfo = .mockMainnet let goerliNetwork: BraveWallet.NetworkInfo = .mockGoerli let solNetwork: BraveWallet.NetworkInfo = .mockSolana + let filMainnet: BraveWallet.NetworkInfo = .mockFilecoinMainnet + let filTestnet: BraveWallet.NetworkInfo = .mockFilecoinTestnet // ETH Asset, balance, price, history let mockETHBalanceAccount1: Double = 0.896 let mockETHPrice: String = "3059.99" // ETH value = $2741.75104 @@ -79,19 +88,53 @@ import Preferences .init(date: Date(), price: "250.00") ] + // FIL Asset, balance, price, history on filecoin mainnet + let mockFILBalanceAccount1: Double = 1 + let mockFILPrice: String = "4.00" // FIL value on mainnet = $4.00 + lazy var mockFILAssetPrice: BraveWallet.AssetPrice = .init( + fromAsset: "fil", toAsset: "usd", + price: mockFILPrice, assetTimeframeChange: "-57.23" + ) + lazy var mockFILPriceHistory: [BraveWallet.AssetTimePrice] = [ + .init(date: Date(timeIntervalSinceNow: -1000), price: "4.06"), + .init(date: Date(), price: mockFILPrice) + ] + // FIL Asset, balance on filecoin testnet + let mockFILBalanceTestnet: Double = 100 // FIL value on testnet = $400.00 + var totalBalance: String { let totalEthBalanceValue: Double = (Double(mockETHAssetPrice.price) ?? 0) * mockETHBalanceAccount1 var totalUSDCBalanceValue: Double = 0 totalUSDCBalanceValue += (Double(mockUSDCAssetPrice.price) ?? 0) * mockUSDCBalanceAccount1 totalUSDCBalanceValue += (Double(mockUSDCAssetPrice.price) ?? 0) * mockUSDCBalanceAccount2 let totalSolBalanceValue: Double = (Double(mockSOLAssetPrice.price) ?? 0) * mockSOLBalance - let totalBalanceValue = totalEthBalanceValue + totalSolBalanceValue + totalUSDCBalanceValue + let totalFilBalanceValue: Double = (Double(mockFILAssetPrice.price) ?? 0) * mockFILBalanceAccount1 + let totalBalanceValue = totalEthBalanceValue + totalSolBalanceValue + totalUSDCBalanceValue + totalFilBalanceValue return currencyFormatter.string(from: NSNumber(value: totalBalanceValue)) ?? "" } private func setupStore() -> PortfolioStore { let mockSOLLamportBalance: UInt64 = 3876535000 // ~3.8765 SOL let formatter = WeiFormatter(decimalFormatStyle: .decimals(precision: 18)) + + // config filecoin on mainnet + let mockFilAccountInfos: [BraveWallet.AccountInfo] = [filAccount1, filAccount2] + let mockFilUserAssets: [BraveWallet.BlockchainToken] = [ + BraveWallet.NetworkInfo.mockFilecoinMainnet.nativeToken.copy(asVisibleAsset: true) + ] + let mockFilBalanceInWei = formatter.weiString( + from: mockFILBalanceAccount1, + radix: .decimal, + decimals: Int(BraveWallet.BlockchainToken.mockFilToken.decimals) + ) ?? "" + // config filecoin on testnet + let mockFilTestnetAccountInfos: [BraveWallet.AccountInfo] = [filTestnetAccount] + let mockFilTestnetBalanceInWei = formatter.weiString( + from: mockFILBalanceTestnet, + radix: .decimal, + decimals: Int(BraveWallet.BlockchainToken.mockFilToken.decimals) + ) ?? "" + // config Solana let mockSolAccountInfos: [BraveWallet.AccountInfo] = [solAccount1, solAccount2] let mockSolUserAssets: [BraveWallet.BlockchainToken] = [ @@ -125,6 +168,10 @@ import Preferences goerliNetwork.nativeToken.copy(asVisibleAsset: true) ] + let mockFilTestnetUserAssets: [BraveWallet.BlockchainToken] = [ + filTestnet.nativeToken.copy(asVisibleAsset: true) + ] + let ethKeyring: BraveWallet.KeyringInfo = .init( id: BraveWallet.KeyringId.default, isKeyringCreated: true, @@ -139,6 +186,20 @@ import Preferences isBackedUp: true, accountInfos: mockSolAccountInfos ) + let filKeyring: BraveWallet.KeyringInfo = .init( + id: .filecoin, + isKeyringCreated: true, + isLocked: false, + isBackedUp: true, + accountInfos: mockFilAccountInfos + ) + let filTestnetKeyring: BraveWallet.KeyringInfo = .init( + id: .filecoinTestnet, + isKeyringCreated: true, + isLocked: false, + isBackedUp: true, + accountInfos: mockFilTestnetAccountInfos + ) // setup test services let keyringService = BraveWallet.TestKeyringService() @@ -149,7 +210,7 @@ import Preferences } keyringService._allAccounts = { $0(.init( - accounts: ethKeyring.accountInfos + solKeyring.accountInfos, + accounts: ethKeyring.accountInfos + solKeyring.accountInfos + filKeyring.accountInfos + filTestnetKeyring.accountInfos, selectedAccount: ethKeyring.accountInfos.first, ethDappSelectedAccount: ethKeyring.accountInfos.first, solDappSelectedAccount: solKeyring.accountInfos.first @@ -164,19 +225,31 @@ import Preferences case .sol: completion([self.solNetwork]) case .fil: - XCTFail("Should not fetch filecoin network") + completion([self.filMainnet, self.filTestnet]) case .btc: XCTFail("Should not fetch bitcoin network") @unknown default: XCTFail("Should not fetch unknown network") } } - rpcService._balance = { accountAddress, _, chainId, completion in - // eth mainnet balance - if chainId == self.ethNetwork.chainId, accountAddress == self.ethAccount1.address { - completion(ethBalanceWei, .success, "") - } else { - completion("", .success, "") + rpcService._balance = { accountAddress, coin, chainId, completion in + // eth balance + if coin == .eth { + if chainId == self.ethNetwork.chainId, accountAddress == self.ethAccount1.address { + completion(ethBalanceWei, .success, "") + } else { + completion("", .success, "") + } + } else { // .fil + if chainId == self.filMainnet.chainId { + if accountAddress == self.filAccount1.address { + completion(mockFilBalanceInWei, .success, "") + } else { + completion("", .success, "") + } + } else { + completion(mockFilTestnetBalanceInWei, .success, "") + } } } rpcService._erc20TokenBalance = { contractAddress, accountAddress, _, completion in @@ -200,7 +273,7 @@ import Preferences walletService._defaultBaseCurrency = { $0(CurrencyCode.usd.code) } let assetRatioService = BraveWallet.TestAssetRatioService() assetRatioService._price = { priceIds, _, _, completion in - completion(true, [self.mockETHAssetPrice, self.mockUSDCAssetPrice, self.mockSOLAssetPrice]) + completion(true, [self.mockETHAssetPrice, self.mockUSDCAssetPrice, self.mockSOLAssetPrice, self.mockFILAssetPrice]) } assetRatioService._priceHistory = { priceId, _, _, completion in switch priceId { @@ -210,6 +283,8 @@ import Preferences completion(true, self.mockETHPriceHistory) case BraveWallet.BlockchainToken.mockUSDCToken.assetRatioId: completion(true, self.mockUSDCPriceHistory) + case "fil": + completion(true, self.mockFILPriceHistory) // for both mainnet and testnet default: completion(false, []) } @@ -218,9 +293,11 @@ import Preferences let mockAssetManager = TestableWalletUserAssetManager() mockAssetManager._getAllVisibleAssetsInNetworkAssets = { networks in [ - NetworkAssets(network: .mockMainnet, tokens: mockEthUserAssets.filter({ $0.visible == true }), sortOrder: 0), - NetworkAssets(network: .mockSolana, tokens: mockSolUserAssets.filter({ $0.visible == true }), sortOrder: 1), - NetworkAssets(network: .mockGoerli, tokens: mockEthGoerliUserAssets.filter({ $0.visible == true }), sortOrder: 2) + NetworkAssets(network: .mockMainnet, tokens: mockEthUserAssets.filter(\.visible), sortOrder: 0), + NetworkAssets(network: .mockSolana, tokens: mockSolUserAssets.filter(\.visible), sortOrder: 1), + NetworkAssets(network: .mockGoerli, tokens: mockEthGoerliUserAssets.filter(\.visible), sortOrder: 2), + NetworkAssets(network: .mockFilecoinMainnet, tokens: mockFilUserAssets.filter(\.visible), sortOrder: 3), + NetworkAssets(network: .mockFilecoinTestnet, tokens: mockFilTestnetUserAssets.filter(\.visible), sortOrder: 4) ].filter { networkAsset in networks.contains(where: { $0 == networkAsset.network }) } } return PortfolioStore( @@ -248,6 +325,7 @@ import Preferences .sink { assetGroups in defer { assetGroupsExpectation.fulfill() } XCTAssertEqual(assetGroups.count, 2) // empty (no balance, price, history), populated + guard let lastUpdatedAssetGroups = assetGroups.last else { XCTFail("Unexpected test result") return @@ -257,8 +335,9 @@ import Preferences XCTFail("Unexpected test result") return } - // ETH on Ethereum mainnet, SOL on Solana mainnet, USDC on Ethereum mainnet, ETH on Goerli - XCTAssertEqual(group.assets.count, 3) + + // ETH on Ethereum mainnet, SOL on Solana mainnet, USDC on Ethereum mainnet, ETH on Goerli, FIL on Filecoin mainnet, FIL on Filecoin testnet + XCTAssertEqual(group.assets.count, 4) // ETH Mainnet (value ~= $2741.7510399999996) XCTAssertEqual(group.assets[safe: 0]?.token.symbol, BraveWallet.BlockchainToken.previewToken.symbol) @@ -268,6 +347,7 @@ import Preferences self.mockETHPriceHistory) XCTAssertEqual(group.assets[safe: 0]?.quantity, String(format: "%.04f", self.mockETHBalanceAccount1)) + // SOL (value = $775.3) XCTAssertEqual(group.assets[safe: 1]?.token.symbol, BraveWallet.BlockchainToken.mockSolToken.symbol) @@ -277,19 +357,33 @@ import Preferences self.mockSOLPriceHistory) XCTAssertEqual(group.assets[safe: 1]?.quantity, String(format: "%.04f", self.mockSOLBalance)) - // USDC (value $0.04) + + // FIL (value $4.00) on mainnet XCTAssertEqual(group.assets[safe: 2]?.token.symbol, - BraveWallet.BlockchainToken.mockUSDCToken.symbol) + BraveWallet.BlockchainToken.mockFilToken.symbol) XCTAssertEqual(group.assets[safe: 2]?.price, - self.mockUSDCAssetPrice.price) + self.mockFILAssetPrice.price) XCTAssertEqual(group.assets[safe: 2]?.history, - self.mockUSDCPriceHistory) + self.mockFILPriceHistory) XCTAssertEqual(group.assets[safe: 2]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + + // USDC (value $0.04) + XCTAssertEqual(group.assets[safe: 3]?.token.symbol, + BraveWallet.BlockchainToken.mockUSDCToken.symbol) + XCTAssertEqual(group.assets[safe: 3]?.price, + self.mockUSDCAssetPrice.price) + XCTAssertEqual(group.assets[safe: 3]?.history, + self.mockUSDCPriceHistory) + XCTAssertEqual(group.assets[safe: 3]?.quantity, String(format: "%.04f", self.mockUSDCBalanceAccount1 + self.mockUSDCBalanceAccount2)) + // ETH Goerli (value = 0), hidden because test networks not selected by default - XCTAssertNil(group.assets[safe: 3]) + // FIL Testnet (value = $400.00), hidden because test networks not selected by default + XCTAssertNil(group.assets[safe: 4]) } .store(in: &cancellables) + // test that `update()` will assign new value to `balance` publisher let balanceExpectation = expectation(description: "update-balance") @@ -338,8 +432,8 @@ import Preferences XCTFail("Unexpected test result") return } - // USDC on Ethereum mainnet, SOL on Solana mainnet, ETH on Ethereum mainnet - XCTAssertEqual(group.assets.count, 4) + // USDC on Ethereum mainnet, SOL on Solana mainnet, ETH on Ethereum mainnet, FIL on Filecoin mainnet and FIL on Filecoin testnet + XCTAssertEqual(group.assets.count, 6) // ETH Goerli (value = $0) XCTAssertEqual(group.assets[safe: 0]?.token.symbol, BraveWallet.BlockchainToken.previewToken.symbol) @@ -349,15 +443,25 @@ import Preferences BraveWallet.BlockchainToken.mockUSDCToken.symbol) XCTAssertEqual(group.assets[safe: 1]?.quantity, String(format: "%.04f", self.mockUSDCBalanceAccount1 + self.mockUSDCBalanceAccount2)) - // SOL (value = $775.3) + // FIL (value = $4.00) on filecoin mainnet XCTAssertEqual(group.assets[safe: 2]?.token.symbol, - BraveWallet.BlockchainToken.mockSolToken.symbol) + BraveWallet.BlockchainToken.mockFilToken.symbol) XCTAssertEqual(group.assets[safe: 2]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + // FIL (value = $400.00) on filecoin testnet + XCTAssertEqual(group.assets[safe: 3]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(group.assets[safe: 3]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + // SOL (value = $775.3) + XCTAssertEqual(group.assets[safe: 4]?.token.symbol, + BraveWallet.BlockchainToken.mockSolToken.symbol) + XCTAssertEqual(group.assets[safe: 4]?.quantity, String(format: "%.04f", self.mockSOLBalance)) // ETH Mainnet (value ~= $2741.7510399999996) - XCTAssertEqual(group.assets[safe: 3]?.token.symbol, + XCTAssertEqual(group.assets[safe: 5]?.token.symbol, BraveWallet.BlockchainToken.previewToken.symbol) - XCTAssertEqual(group.assets[safe: 3]?.quantity, + XCTAssertEqual(group.assets[safe: 5]?.quantity, String(format: "%.04f", self.mockETHBalanceAccount1)) }.store(in: &cancellables) @@ -368,10 +472,10 @@ import Preferences isHidingSmallBalances: store.filters.isHidingSmallBalances, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: true, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { .init(isSelected: true, model: $0) } )) @@ -399,8 +503,8 @@ import Preferences XCTFail("Unexpected test result") return } - // ETH on Ethereum mainnet, SOL on Solana mainnet - XCTAssertEqual(group.assets.count, 2) // USDC, ETH Goerli hidden for small balance + // ETH on Ethereum mainnet, SOL on Solana mainnet, FIL on Filecoin mainnet and testnet + XCTAssertEqual(group.assets.count, 4) // USDC, ETH Goerli hidden for small balance // ETH Mainnet (value ~= $2741.7510399999996) XCTAssertEqual(group.assets[safe: 0]?.token.symbol, BraveWallet.BlockchainToken.previewToken.symbol) @@ -411,8 +515,18 @@ import Preferences BraveWallet.BlockchainToken.mockSolToken.symbol) XCTAssertEqual(group.assets[safe: 1]?.quantity, String(format: "%.04f", self.mockSOLBalance)) + // FIL (value = $400) on Filecoin testnet + XCTAssertEqual(group.assets[safe: 2]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(group.assets[safe: 2]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + // FIL (value = $4) on Filecoin mainnet + XCTAssertEqual(group.assets[safe: 3]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(group.assets[safe: 3]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) // USDC (value = $0.04), hidden - XCTAssertNil(group.assets[safe: 2]) + XCTAssertNil(group.assets[safe: 4]) }.store(in: &cancellables) store.saveFilters(.init( groupBy: store.filters.groupBy, @@ -420,10 +534,10 @@ import Preferences isHidingSmallBalances: true, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: true, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { .init(isSelected: true, model: $0) } )) @@ -451,8 +565,8 @@ import Preferences XCTFail("Unexpected test result") return } - // ETH on Ethereum mainnet, SOL on Solana mainnet, USDC on Ethereum mainnet, ETH on Goerli - XCTAssertEqual(group.assets.count, 4) + // ETH on Ethereum mainnet, SOL on Solana mainnet, USDC on Ethereum mainnet, ETH on Goerli, FIL on mainnet and testnet + XCTAssertEqual(group.assets.count, 6) // ETH Mainnet (value ~= $2741.7510399999996) XCTAssertEqual(group.assets[safe: 0]?.token.symbol, BraveWallet.BlockchainToken.previewToken.symbol) @@ -463,15 +577,25 @@ import Preferences BraveWallet.BlockchainToken.mockSolToken.symbol) XCTAssertEqual(group.assets[safe: 1]?.quantity, String(format: "%.04f", self.mockSOLBalance)) - // USDC (value = $0.03, ethAccount2 hidden!) + // FIL (value = $400) on testnet XCTAssertEqual(group.assets[safe: 2]?.token.symbol, - BraveWallet.BlockchainToken.mockUSDCToken.symbol) + BraveWallet.BlockchainToken.mockFilToken.symbol) XCTAssertEqual(group.assets[safe: 2]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + // FIL (value = $4) on mainnet + XCTAssertEqual(group.assets[safe: 3]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(group.assets[safe: 3]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + // USDC (value = $0.03, ethAccount2 hidden!) + XCTAssertEqual(group.assets[safe: 4]?.token.symbol, + BraveWallet.BlockchainToken.mockUSDCToken.symbol) + XCTAssertEqual(group.assets[safe: 4]?.quantity, String(format: "%.04f", self.mockUSDCBalanceAccount1)) // verify account 2 hidden // ETH Goerli (value = $0) - XCTAssertEqual(group.assets[safe: 03]?.token.symbol, + XCTAssertEqual(group.assets[safe: 5]?.token.symbol, self.goerliNetwork.nativeToken.symbol) - XCTAssertEqual(group.assets[safe: 3]?.quantity, String(format: "%.04f", 0)) + XCTAssertEqual(group.assets[safe: 5]?.quantity, String(format: "%.04f", 0)) }.store(in: &cancellables) store.saveFilters(.init( groupBy: store.filters.groupBy, @@ -479,10 +603,10 @@ import Preferences isHidingSmallBalances: false, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { // deselect ethAccount2 + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { // deselect ethAccount2 .init(isSelected: $0 != ethAccount2, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { .init(isSelected: true, model: $0) } )) @@ -510,24 +634,34 @@ import Preferences XCTFail("Unexpected test result") return } - // ETH on Ethereum mainnet, USDC on Ethereum mainnet, ETH on Goerli - XCTAssertEqual(group.assets.count, 3) + // ETH on Ethereum mainnet, USDC on Ethereum mainnet, ETH on Goerli, FIL on mainnet and testnet + XCTAssertEqual(group.assets.count, 5) // ETH Mainnet (value ~= $2741.7510399999996) XCTAssertEqual(group.assets[safe: 0]?.token.symbol, BraveWallet.BlockchainToken.previewToken.symbol) XCTAssertEqual(group.assets[safe: 0]?.quantity, String(format: "%.04f", self.mockETHBalanceAccount1)) - // USDC (value = $0.04) + // FIL (value = $400) on testnet XCTAssertEqual(group.assets[safe: 1]?.token.symbol, - BraveWallet.BlockchainToken.mockUSDCToken.symbol) + BraveWallet.BlockchainToken.mockFilToken.symbol) XCTAssertEqual(group.assets[safe: 1]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + // FIL (value = $4) on mainnet + XCTAssertEqual(group.assets[safe: 2]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(group.assets[safe: 2]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + // USDC (value = $0.04) + XCTAssertEqual(group.assets[safe: 3]?.token.symbol, + BraveWallet.BlockchainToken.mockUSDCToken.symbol) + XCTAssertEqual(group.assets[safe: 3]?.quantity, String(format: "%.04f", self.mockUSDCBalanceAccount1 + self.mockUSDCBalanceAccount2)) // ETH Goerli (value = $0) - XCTAssertEqual(group.assets[safe: 2]?.token.symbol, + XCTAssertEqual(group.assets[safe: 4]?.token.symbol, self.goerliNetwork.nativeToken.symbol) - XCTAssertEqual(group.assets[safe: 2]?.quantity, String(format: "%.04f", 0)) + XCTAssertEqual(group.assets[safe: 4]?.quantity, String(format: "%.04f", 0)) // SOL (value = $0, SOL networks hidden) - XCTAssertNil(group.assets[safe: 3]) + XCTAssertNil(group.assets[safe: 5]) }.store(in: &cancellables) store.saveFilters(.init( groupBy: store.filters.groupBy, @@ -535,11 +669,11 @@ import Preferences isHidingSmallBalances: false, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: true, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { // only select Ethereum networks - .init(isSelected: $0.coin == .eth, model: $0) + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { // only select Ethereum networks + .init(isSelected: $0.coin == .eth || $0.coin == .fil, model: $0) } )) await fulfillment(of: [networksExpectation], timeout: 1) @@ -563,11 +697,14 @@ import Preferences return } // grouping by .account; 1 for each of the 4 accounts - XCTAssertEqual(lastUpdatedAssetGroups.count, 4) + XCTAssertEqual(lastUpdatedAssetGroups.count, 7) guard let ethAccount1Group = lastUpdatedAssetGroups[safe: 0], let solAccount1Group = lastUpdatedAssetGroups[safe: 1], - let ethAccount2Group = lastUpdatedAssetGroups[safe: 2], - let solAccount2Group = lastUpdatedAssetGroups[safe: 3] else { + let filTestnetAccountGroup = lastUpdatedAssetGroups[safe: 2], + let filAccount1Group = lastUpdatedAssetGroups[safe: 3], + let ethAccount2Group = lastUpdatedAssetGroups[safe: 4], + let solAccount2Group = lastUpdatedAssetGroups[safe: 5], + let filAccount2Group = lastUpdatedAssetGroups[safe: 6] else { XCTFail("Unexpected test result") return } @@ -596,6 +733,22 @@ import Preferences XCTAssertEqual(solAccount1Group.assets[safe: 0]?.quantity, String(format: "%.04f", self.mockSOLBalance)) + XCTAssertEqual(filTestnetAccountGroup.groupType, .account(self.filTestnetAccount)) + XCTAssertEqual(filTestnetAccountGroup.assets.count, 1) + // FIL (value = $400) + XCTAssertEqual(filTestnetAccountGroup.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filTestnetAccountGroup.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + + XCTAssertEqual(filAccount1Group.groupType, .account(self.filAccount1)) + XCTAssertEqual(filAccount1Group.assets.count, 1) + // FIL (value = $4) + XCTAssertEqual(filAccount1Group.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filAccount1Group.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + XCTAssertEqual(ethAccount2Group.groupType, .account(self.ethAccount2)) XCTAssertEqual(ethAccount2Group.assets.count, 3) // ETH Mainnet, USDC, ETH Goerli // USDC (value $0.01) @@ -618,6 +771,13 @@ import Preferences XCTAssertEqual(solAccount2Group.assets[safe: 0]?.token.symbol, BraveWallet.BlockchainToken.mockSolToken.symbol) XCTAssertEqual(solAccount2Group.assets[safe: 0]?.quantity, String(format: "%.04f", 0)) + + XCTAssertEqual(filAccount2Group.groupType, .account(self.filAccount2)) + XCTAssertEqual(filAccount2Group.assets.count, 1) + // FIL (value = $0) + XCTAssertEqual(filAccount2Group.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filAccount2Group.assets[safe: 0]?.quantity, String(format: "%.04f", 0)) } .store(in: &cancellables) store.saveFilters(.init( @@ -626,10 +786,10 @@ import Preferences isHidingSmallBalances: store.filters.isHidingSmallBalances, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: true, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { .init(isSelected: true, model: $0) } )) @@ -648,9 +808,11 @@ import Preferences return } // grouping by .account; 1 for each of the 2 accounts selected accounts - XCTAssertEqual(lastUpdatedAssetGroups.count, 2) + XCTAssertEqual(lastUpdatedAssetGroups.count, 4) guard let ethAccount1Group = lastUpdatedAssetGroups[safe: 0], - let solAccountGroup = lastUpdatedAssetGroups[safe: 1] else { + let solAccountGroup = lastUpdatedAssetGroups[safe: 1], + let filTestnetAccountGroup = lastUpdatedAssetGroups[safe: 2], + let filAccount1Group = lastUpdatedAssetGroups[safe: 3] else { XCTFail("Unexpected test result") return } @@ -671,9 +833,27 @@ import Preferences BraveWallet.BlockchainToken.mockSolToken.symbol) XCTAssertEqual(solAccountGroup.assets[safe: 0]?.quantity, String(format: "%.04f", self.mockSOLBalance)) - // ethAccount2 hidden as it's de-selected, solAccount2 hidden for small balance - XCTAssertNil(lastUpdatedAssetGroups[safe: 2]) - XCTAssertNil(lastUpdatedAssetGroups[safe: 3]) + + XCTAssertEqual(filTestnetAccountGroup.groupType, .account(self.filTestnetAccount)) + XCTAssertEqual(filTestnetAccountGroup.assets.count, 1) + // FIL (value = $400) + XCTAssertEqual(filTestnetAccountGroup.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filTestnetAccountGroup.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + + XCTAssertEqual(filAccount1Group.groupType, .account(self.filAccount1)) + XCTAssertEqual(filAccount1Group.assets.count, 1) + // FIL (value = $4) + XCTAssertEqual(filAccount1Group.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filAccount1Group.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + + // ethAccount2 hidden as it's de-selected, solAccount2 hidden for small balance, filAccount2 hidden for small balance + XCTAssertNil(lastUpdatedAssetGroups[safe: 4]) + XCTAssertNil(lastUpdatedAssetGroups[safe: 5]) + XCTAssertNil(lastUpdatedAssetGroups[safe: 6]) } .store(in: &cancellables) store.saveFilters(.init( @@ -682,10 +862,10 @@ import Preferences isHidingSmallBalances: true, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: $0 != ethAccount2, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { .init(isSelected: true, model: $0) } )) @@ -709,11 +889,13 @@ import Preferences XCTFail("Unexpected test result") return } - // grouping by .network; 1 for each of the 2 networks, with Goerli group hidden due to 0 balance. - XCTAssertEqual(lastUpdatedAssetGroups.count, 3) + // grouping by .network; 1 for each of the 2 networks + XCTAssertEqual(lastUpdatedAssetGroups.count, 5) guard let ethMainnetGroup = lastUpdatedAssetGroups[safe: 0], let solMainnetGroup = lastUpdatedAssetGroups[safe: 1], - let ethGoerliGroup = lastUpdatedAssetGroups[safe: 2] else { + let filTestnetGroup = lastUpdatedAssetGroups[safe: 2], + let filMainnetGroup = lastUpdatedAssetGroups[safe: 3], + let ethGoerliGroup = lastUpdatedAssetGroups[safe: 4] else { XCTFail("Unexpected test result") return } @@ -738,6 +920,22 @@ import Preferences XCTAssertEqual(solMainnetGroup.assets[safe: 0]?.quantity, String(format: "%.04f", self.mockSOLBalance)) + XCTAssertEqual(filTestnetGroup.groupType, .network(.mockFilecoinTestnet)) + XCTAssertEqual(filTestnetGroup.assets.count, 1) // FIL on testnet + // FIL (value = $400) + XCTAssertEqual(filTestnetGroup.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filTestnetGroup.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + + XCTAssertEqual(filMainnetGroup.groupType, .network(.mockFilecoinMainnet)) + XCTAssertEqual(filMainnetGroup.assets.count, 1) // FIL on mainnet + // FIL (value = $4) + XCTAssertEqual(filMainnetGroup.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filMainnetGroup.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) + XCTAssertEqual(ethGoerliGroup.groupType, .network(.mockGoerli)) XCTAssertEqual(ethGoerliGroup.assets.count, 1) // ETH Goerli // ETH Goerli (value = $0) @@ -752,10 +950,10 @@ import Preferences isHidingSmallBalances: store.filters.isHidingSmallBalances, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: true, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { .init(isSelected: true, model: $0) } )) @@ -773,9 +971,11 @@ import Preferences XCTFail("Unexpected test result") return } - // grouping by .network; 1 group for Solana network - XCTAssertEqual(lastUpdatedAssetGroups.count, 1) - guard let solMainnetGroup = lastUpdatedAssetGroups[safe: 0] else { + // grouping by .network; 1 group for Solana network, 1 group for Filecoin mainnet, 1 group for Filecoin testnet + XCTAssertEqual(lastUpdatedAssetGroups.count, 3) + guard let solMainnetGroup = lastUpdatedAssetGroups[safe: 0], + let filTestnetGroup = lastUpdatedAssetGroups[safe: 1], + let filMainnetGroup = lastUpdatedAssetGroups[safe: 2] else { XCTFail("Unexpected test result") return } @@ -786,10 +986,26 @@ import Preferences BraveWallet.BlockchainToken.mockSolToken.symbol) XCTAssertEqual(solMainnetGroup.assets[safe: 0]?.quantity, String(format: "%.04f", self.mockSOLBalance)) + + XCTAssertEqual(filTestnetGroup.groupType, .network(.mockFilecoinTestnet)) + XCTAssertEqual(filTestnetGroup.assets.count, 1) // FIL + // FIL (value = $400) + XCTAssertEqual(filTestnetGroup.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filTestnetGroup.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceTestnet)) + + XCTAssertEqual(filMainnetGroup.groupType, .network(.mockFilecoinMainnet)) + XCTAssertEqual(filMainnetGroup.assets.count, 1) // FIL + // FIL (value = $4) + XCTAssertEqual(filMainnetGroup.assets[safe: 0]?.token.symbol, + BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertEqual(filMainnetGroup.assets[safe: 0]?.quantity, + String(format: "%.04f", self.mockFILBalanceAccount1)) // eth mainnet group hidden as network de-selected - XCTAssertNil(lastUpdatedAssetGroups[safe: 1]) + XCTAssertNil(lastUpdatedAssetGroups[safe: 3]) // goerli network group hidden for small balance - XCTAssertNil(lastUpdatedAssetGroups[safe: 2]) + XCTAssertNil(lastUpdatedAssetGroups[safe: 4]) } .store(in: &cancellables) store.saveFilters(.init( @@ -798,10 +1014,10 @@ import Preferences isHidingSmallBalances: true, isHidingUnownedNFTs: store.filters.isHidingUnownedNFTs, isShowingNFTNetworkLogo: store.filters.isShowingNFTNetworkLogo, - accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2].map { + accounts: [ethAccount1, ethAccount2, solAccount1, solAccount2, filAccount1, filAccount2, filTestnetAccount].map { .init(isSelected: true, model: $0) }, - networks: [ethNetwork, goerliNetwork, solNetwork].map { // hide ethNetwork + networks: [ethNetwork, goerliNetwork, solNetwork, filMainnet, filTestnet].map { // hide ethNetwork .init(isSelected: $0 != ethNetwork, model: $0) } )) diff --git a/Tests/BraveWalletTests/SelectAccountTokenStoreTests.swift b/Tests/BraveWalletTests/SelectAccountTokenStoreTests.swift index a46914b8848..e742d37154f 100644 --- a/Tests/BraveWalletTests/SelectAccountTokenStoreTests.swift +++ b/Tests/BraveWalletTests/SelectAccountTokenStoreTests.swift @@ -25,7 +25,9 @@ import Preferences .mockUSDCToken.copy(asVisibleAsset: true).then { $0.chainId = BraveWallet.GoerliChainId }, .mockSolToken.copy(asVisibleAsset: true), .mockSpdToken.copy(asVisibleAsset: false), // not visible - .mockSolanaNFTToken.copy(asVisibleAsset: true).then { $0.chainId = BraveWallet.SolanaTestnet } + .mockSolanaNFTToken.copy(asVisibleAsset: true).then { $0.chainId = BraveWallet.SolanaTestnet }, + .mockFilToken.copy(asVisibleAsset: true), + .mockFilToken.copy(asVisibleAsset: true).then { $0.chainId = BraveWallet.FilecoinTestnet} ] private var allUserAssetsInNetworkAssets: [NetworkAssets] { [ @@ -48,15 +50,27 @@ import Preferences network: .mockSolanaTestnet, tokens: [allUserAssets[4]], sortOrder: 3 + ), + NetworkAssets( + network: .mockFilecoinMainnet, + tokens: [allUserAssets[5]], + sortOrder: 4 + ), + NetworkAssets( + network: .mockFilecoinTestnet, + tokens: [allUserAssets[6]], + sortOrder: 5 ) ] } - private let allNetworks: [BraveWallet.NetworkInfo] = [ - .mockMainnet, - .mockGoerli, - .mockSolana, - .mockSolanaTestnet + private let allNetworks: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ + .eth: [.mockMainnet, + .mockGoerli], + .sol: [.mockSolana, + .mockSolanaTestnet], + .fil: [.mockFilecoinMainnet, + .mockFilecoinTestnet] ] private let mockEthAccount2: BraveWallet.AccountInfo = .init( @@ -92,6 +106,8 @@ import Preferences name: "sol mock nft name", description: "sol mock nft description" ) + let mockFILBalance: Double = 1 + let mockFILPrice: String = "4.06" // FIL value = $4.06 let ethBalanceWei = formatter.weiString( from: mockETHBalance, @@ -112,10 +128,19 @@ import Preferences let mockSOLAssetPrice: BraveWallet.AssetPrice = .init( fromAsset: "sol", toAsset: "usd", price: mockSOLPrice, assetTimeframeChange: "-57.23") + let filBalanceWei = formatter.weiString( + from: mockFILBalance, + radix: .decimal, + decimals: Int(allUserAssets[5].decimals) + ) ?? "" + let mockFILAssetPrice: BraveWallet.AssetPrice = .init( + fromAsset: "fil", toAsset: "usd", + price: mockFILPrice, assetTimeframeChange: "-57.23") let keyringService = BraveWallet.TestKeyringService() keyringService._keyringInfo = { keyringId, completion in - if keyringId == BraveWallet.KeyringId.default { + switch keyringId { + case .default: let keyring: BraveWallet.KeyringInfo = .init( id: BraveWallet.KeyringId.default, isKeyringCreated: true, @@ -124,7 +149,7 @@ import Preferences accountInfos: [.mockEthAccount, self.mockEthAccount2] ) completion(keyring) - } else { + case .solana: let keyring: BraveWallet.KeyringInfo = .init( id: BraveWallet.KeyringId.solana, isKeyringCreated: true, @@ -133,14 +158,24 @@ import Preferences accountInfos: [.mockSolAccount] ) completion(keyring) + case .filecoin: + completion(.mockFilecoinKeyringInfo) + case .filecoinTestnet: + completion(.mockFilecoinTestnetKeyringInfo) + default: + completion(.mockDefaultKeyringInfo) } } let rpcService = BraveWallet.TestJsonRpcService() rpcService._allNetworks = { coin, completion in - completion(self.allNetworks.filter { $0.coin == coin }) + completion(self.allNetworks[coin] ?? []) } - rpcService._balance = { accountAddress, _, _, completion in - completion(ethBalanceWei, .success, "") // eth balance for both eth accounts + rpcService._balance = { accountAddress, coin, _, completion in + if coin == .eth { + completion(ethBalanceWei, .success, "") // eth balance for both eth accounts + } else { // .fil + completion(filBalanceWei, .success, "") + } } rpcService._erc20TokenBalance = { contractAddress, accountAddress, _, completion in if accountAddress == self.mockEthAccount2.address { @@ -191,7 +226,7 @@ import Preferences } let assetRatioService = BraveWallet.TestAssetRatioService() assetRatioService._price = { priceIds, _, _, completion in - completion(true, [mockETHAssetPrice, mockUSDCAssetPrice, mockSOLAssetPrice]) + completion(true, [mockETHAssetPrice, mockUSDCAssetPrice, mockSOLAssetPrice, mockFILAssetPrice]) } let store = SelectAccountTokenStore( @@ -217,7 +252,7 @@ import Preferences XCTFail("Unexpected test setup") return } - XCTAssertEqual(accountSections.count, 3) // 2 eth accounts, 1 sol accounts + XCTAssertEqual(accountSections.count, 5) // 2 eth accounts, 1 sol accounts, 2 filecoin account, 1 filecoin testnet accout XCTAssertEqual(accountSections[safe: 0]?.account, .mockEthAccount) XCTAssertEqual(accountSections[safe: 0]?.tokenBalances[safe: 0]?.token, self.allUserAssets[0]) // ETH @@ -231,6 +266,12 @@ import Preferences XCTAssertEqual(accountSections[safe: 2]?.tokenBalances[safe: 0]?.token, self.allUserAssets[2]) // SOL XCTAssertEqual(accountSections[safe: 2]?.tokenBalances[safe: 1]?.token, self.allUserAssets[4]) // Solana NFT XCTAssertNil(accountSections[safe: 2]?.tokenBalances[safe: 2]) // `mockSpdToken` is not visible + + XCTAssertEqual(accountSections[safe: 3]?.account, .mockFilAccount) + XCTAssertEqual(accountSections[safe: 3]?.tokenBalances[safe: 0]?.token, self.allUserAssets[5]) // FIL on mainnet + + XCTAssertEqual(accountSections[safe: 4]?.account, .mockFilTestnetAccount) + XCTAssertEqual(accountSections[safe: 4]?.tokenBalances[safe: 0]?.token, self.allUserAssets[6]) // FIL on testnet }.store(in: &cancellables) await store.update() @@ -238,7 +279,7 @@ import Preferences // verify `filteredAccountSections` which get displayed in UI var accountSections = store.filteredAccountSections - XCTAssertEqual(accountSections.count, 3) // 2 eth accounts, 1 sol accounts + XCTAssertEqual(accountSections.count, 5) // 2 eth accounts, 1 sol accounts, 2 fil accounts // Account 1 XCTAssertEqual(accountSections[safe: 0]?.account, .mockEthAccount) @@ -273,10 +314,24 @@ import Preferences XCTAssertEqual(accountSections[safe: 2]?.tokenBalances[safe: 1]?.balance, mockNFTBalance) XCTAssertEqual(accountSections[safe: 2]?.tokenBalances[safe: 1]?.nftMetadata, mockNFTMetadata) + // Filecoin account on mainnet + XCTAssertEqual(accountSections[safe: 3]?.account, .mockFilAccount) + XCTAssertEqual(accountSections[safe: 3]?.tokenBalances[safe: 0]?.token, self.allUserAssets[5]) // FIL + XCTAssertEqual(accountSections[safe: 3]?.tokenBalances[safe: 0]?.network.chainId, BraveWallet.FilecoinMainnet) + XCTAssertEqual(accountSections[safe: 3]?.tokenBalances[safe: 0]?.balance, mockFILBalance) + XCTAssertEqual(accountSections[safe: 3]?.tokenBalances[safe: 0]?.price, "$4.06") + + // Filecoin account on testnet + XCTAssertEqual(accountSections[safe: 4]?.account, .mockFilTestnetAccount) + XCTAssertEqual(accountSections[safe: 4]?.tokenBalances[safe: 0]?.token, self.allUserAssets[6]) // FIL + XCTAssertEqual(accountSections[safe: 4]?.tokenBalances[safe: 0]?.network.chainId, BraveWallet.FilecoinTestnet) + XCTAssertEqual(accountSections[safe: 4]?.tokenBalances[safe: 0]?.balance, mockFILBalance) + XCTAssertEqual(accountSections[safe: 4]?.tokenBalances[safe: 0]?.price, "$4.06") + // Test with zero balances shown store.isHidingZeroBalances = false accountSections = store.filteredAccountSections - XCTAssertEqual(accountSections.count, 3) // 2 eth accounts, 1 sol accounts + XCTAssertEqual(accountSections.count, 5) // 2 eth accounts, 1 sol accounts, 2 fil accounts // Account 1 XCTAssertEqual(accountSections[safe: 0]?.account, .mockEthAccount) @@ -292,6 +347,14 @@ import Preferences XCTAssertEqual(accountSections[safe: 2]?.account, .mockSolAccount) XCTAssertEqual(accountSections[safe: 2]?.tokenBalances[safe: 0]?.token, self.allUserAssets[2]) // SOL XCTAssertEqual(accountSections[safe: 2]?.tokenBalances[safe: 1]?.token, self.allUserAssets[4]) // Solana NFT + + // Filecoin account on mainnet + XCTAssertEqual(accountSections[safe: 3]?.account, .mockFilAccount) + XCTAssertEqual(accountSections[safe: 3]?.tokenBalances[safe: 0]?.token, self.allUserAssets[5]) // FIL + + // Filecoin account on testnet + XCTAssertEqual(accountSections[safe: 4]?.account, .mockFilTestnetAccount) + XCTAssertEqual(accountSections[safe: 4]?.tokenBalances[safe: 0]?.token, self.allUserAssets[6]) // FIL } func testNetworkFilter() { @@ -300,6 +363,7 @@ import Preferences let walletService = BraveWallet.TestBraveWalletService() walletService._addObserver = { _ in } let assetRatioService = BraveWallet.TestAssetRatioService() + let mockFilecoinTestToken: BraveWallet.BlockchainToken = .mockFilToken.copy(asVisibleAsset: true).then { $0.chainId = BraveWallet.FilecoinTestnet } let store = SelectAccountTokenStore( didSelect: { _, _ in }, @@ -343,6 +407,26 @@ import Preferences balance: 4 ) ] + ), + .init( + account: .mockFilAccount, + tokenBalances: [ + .init( + token: .mockFilToken, + network: .mockFilecoinMainnet, + balance: 1 + ) + ] + ), + .init( + account: .mockFilTestnetAccount, + tokenBalances: [ + .init( + token: mockFilecoinTestToken, + network: .mockFilecoinTestnet, + balance: 2 + ) + ] ) ] // all networks @@ -350,7 +434,9 @@ import Preferences .init(isSelected: true, model: .mockMainnet), .init(isSelected: true, model: .mockGoerli), .init(isSelected: true, model: .mockSolana), - .init(isSelected: true, model: .mockSolanaTestnet) + .init(isSelected: true, model: .mockSolanaTestnet), + .init(isSelected: true, model: .mockFilecoinMainnet), + .init(isSelected: true, model: .mockFilecoinTestnet) ] XCTAssertEqual(store.filteredAccountSections, store.accountSections) // Ethereum mainnet @@ -383,5 +469,35 @@ import Preferences ] ) ]) + // Filecoin mainnet + store.networkFilters = [.init(isSelected: true, model: .mockFilecoinMainnet)] + XCTAssertEqual(store.filteredAccountSections.count, 1) + XCTAssertEqual(store.filteredAccountSections, [ + .init( + account: .mockFilAccount, + tokenBalances: [ + .init( + token: .mockFilToken, + network: .mockFilecoinMainnet, + balance: 1 + ) + ] + ) + ]) + // Filecoin testnet + store.networkFilters = [.init(isSelected: true, model: .mockFilecoinTestnet)] + XCTAssertEqual(store.filteredAccountSections.count, 1) + XCTAssertEqual(store.filteredAccountSections, [ + .init( + account: .mockFilTestnetAccount, + tokenBalances: [ + .init( + token: mockFilecoinTestToken, + network: .mockFilecoinTestnet, + balance: 2 + ) + ] + ) + ]) } } diff --git a/Tests/BraveWalletTests/SendTokenStoreTests.swift b/Tests/BraveWalletTests/SendTokenStoreTests.swift index 488ef14f317..a34bc14f674 100644 --- a/Tests/BraveWalletTests/SendTokenStoreTests.swift +++ b/Tests/BraveWalletTests/SendTokenStoreTests.swift @@ -7,9 +7,17 @@ import XCTest import Combine import BraveCore import BigNumber +import Preferences @testable import BraveWallet class SendTokenStoreTests: XCTestCase { + override func setUp() { + Preferences.Wallet.showTestNetworks.value = true + } + override func tearDown() { + Preferences.Wallet.showTestNetworks.reset() + } + private var cancellables: Set = [] private let batSymbol = "BAT" @@ -534,6 +542,33 @@ class SendTokenStoreTests: XCTestCase { } } + /// Test making a FIL Send transaction on filecoin mainnet + func testMakeSendFILTransaction() { + let (keyringService, rpcService, walletService, ethTxManagerProxy, solTxManagerProxy, mockAssetManager) = setupServices() + let store = SendTokenStore( + keyringService: keyringService, + rpcService: rpcService, + walletService: walletService, + txService: MockTxService(), + blockchainRegistry: MockBlockchainRegistry(), + assetRatioService: MockAssetRatioService(), + ethTxManagerProxy: ethTxManagerProxy, + solTxManagerProxy: solTxManagerProxy, + prefilledToken: .previewToken, + ipfsApi: TestIpfsAPI(), + userAssetManager: mockAssetManager + ) + store.selectedSendToken = .mockFilToken + let ex = expectation(description: "send-fil-transaction") + store.sendToken(amount: "1") { success, _ in + defer { ex.fulfill() } + XCTAssertTrue(success) + } + waitForExpectations(timeout: 3) { error in + XCTAssertNil(error) + } + } + /// Test Solana System Program transaction is created with correct lamports value for the `mockSolToken` (9 decimals) func testSendSolAmount() { let mockBalance: UInt64 = 47 @@ -1017,7 +1052,9 @@ class SendTokenStoreTests: XCTestCase { store.$resolvedAddress .dropFirst(3) // Initial value, reset to nil in `sendAddress` didSet, reset to nil in `validateEthereumSendAddress` .sink { resolvedAddress in - defer { resolvedAddressExpectation.fulfill() } + defer { + resolvedAddressExpectation.fulfill() + } XCTAssertEqual(resolvedAddress, expectedAddress) }.store(in: &cancellables) store.sendAddress = domain @@ -1482,28 +1519,50 @@ class SendTokenStoreTests: XCTestCase { await fulfillment(of: [didSelectSendTokenExpectation, setSelectedAccountExpectation, setNetworkExpectation], timeout: 1) } - /// Test `didSelect(account:token:)` with a new token that is on a different coin type (ex. Ethereum -> Solana). + /// Test `didSelect(account:token:)` with a new token that is on a different coin type (ex. Ethereum -> Solana, Solana -> Filecoin). @MainActor func testDidSelectCoinTypeSwitch() async { let solMainnet: BraveWallet.BlockchainToken = BraveWallet.NetworkInfo.mockSolana.nativeToken .copy(asVisibleAsset: true) + let filTokenMainnet: BraveWallet.BlockchainToken = .mockFilToken.copy(asVisibleAsset: true) var selectedNetwork: BraveWallet.NetworkInfo = .mockGoerli - let allNetworks: [BraveWallet.NetworkInfo] = [.mockGoerli, .mockSolana] + var selectedNetworkToBe: BraveWallet.NetworkInfo = .mockSolana + var selectedAccount: BraveWallet.AccountInfo = account + let allNetworks: [BraveWallet.NetworkInfo] = [.mockGoerli, .mockSolana, .mockFilecoinMainnet] let (keyringService, rpcService, walletService, ethTxManagerProxy, solTxManagerProxy, mockAssetManager) = setupServices( - selectedAccount: account, - userAssets: [.mockGoerli: [usdcGoerli], .mockSolana: [solMainnet]], + selectedAccount: selectedAccount, + userAssets: [.mockGoerli: [usdcGoerli], .mockSolana: [solMainnet], .mockFilecoinMainnet: [filTokenMainnet]], selectedCoin: .eth, allNetworks: allNetworks ) + + let didSelectSendFilTokenExpectation = expectation(description: "didSelectSendFilTokenExpectation") + let setSelectedFilAccountExpectation = expectation(description: "setSelectedFilAccountExpectation") + let setFilNetworkExpectation = expectation(description: "setFilNetworkExpectation") + + let didSelectSendTokenExpectation = expectation(description: "didSelectSendTokenExpectation") let setSelectedAccountExpectation = expectation(description: "setSelectedAccountExpectation") + let setNetworkExpectation = expectation(description: "setNetworkExpectation") + keyringService._setSelectedAccount = { accountId, completion in - defer { setSelectedAccountExpectation.fulfill() } - XCTAssertEqual(accountId.address, BraveWallet.AccountInfo.mockSolAccount.address) + defer { + if accountId.coin == .sol { + setSelectedAccountExpectation.fulfill() + } else { + setSelectedFilAccountExpectation.fulfill() + } + } + XCTAssertEqual(accountId.address, selectedAccount.address) completion(true) } - let setNetworkExpectation = expectation(description: "setNetworkExpectation") rpcService._setNetwork = { chainId, coin, origin, completion in - defer { setNetworkExpectation.fulfill() } - XCTAssertEqual(chainId, solMainnet.chainId) + defer { + if coin == .sol { + setNetworkExpectation.fulfill() + } else { + setFilNetworkExpectation.fulfill() + } + } + XCTAssertEqual(chainId, selectedNetworkToBe.chainId) selectedNetwork = allNetworks.first(where: { $0.chainId == chainId }) ?? selectedNetwork completion(true) } @@ -1536,15 +1595,34 @@ class SendTokenStoreTests: XCTestCase { await fulfillment(of: [sendTokenExpectation], timeout: 1) cancellables.removeAll() - let didSelectSendTokenExpectation = expectation(description: "didSelectSendTokenExpectation") + // select solana mainnet + selectedAccount = .mockSolAccount store.$selectedSendToken .dropFirst() .sink { selectedSendToken in - defer { didSelectSendTokenExpectation.fulfill() } + defer { + didSelectSendTokenExpectation.fulfill() + } XCTAssertEqual(selectedSendToken, solMainnet) } .store(in: &cancellables) store.didSelect(account: .mockSolAccount, token: solMainnet) await fulfillment(of: [didSelectSendTokenExpectation, setSelectedAccountExpectation, setNetworkExpectation], timeout: 1) + cancellables.removeAll() + + // select filecoin mainnet + selectedAccount = .mockFilAccount + selectedNetworkToBe = .mockFilecoinMainnet + store.$selectedSendToken + .dropFirst() + .sink { selectedSendToken in + defer { + didSelectSendFilTokenExpectation.fulfill() + } + XCTAssertEqual(selectedSendToken, filTokenMainnet) + } + .store(in: &cancellables) + store.didSelect(account: .mockFilAccount, token: filTokenMainnet) + await fulfillment(of: [didSelectSendFilTokenExpectation, setSelectedFilAccountExpectation, setFilNetworkExpectation], timeout: 1) } } diff --git a/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift b/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift index eb1ac2b4b7e..93c8427e3c0 100644 --- a/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift +++ b/Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift @@ -23,13 +23,15 @@ import Preferences private func setupStore( selectedNetworkForCoinType: [BraveWallet.CoinType: BraveWallet.NetworkInfo] = [ .eth : BraveWallet.NetworkInfo.mockMainnet, - .sol : BraveWallet.NetworkInfo.mockSolana + .sol : BraveWallet.NetworkInfo.mockSolana, + .fil : BraveWallet.NetworkInfo.mockFilecoinMainnet ], allNetworksForCoinType: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ .eth: [.mockMainnet, .mockGoerli], - .sol: [.mockSolana, .mockSolanaTestnet] + .sol: [.mockSolana, .mockSolanaTestnet], + .fil: [.mockFilecoinMainnet, .mockFilecoinTestnet] ], - accountInfos: [BraveWallet.AccountInfo] = [.mockEthAccount, .mockSolAccount], + accountInfos: [BraveWallet.AccountInfo] = [.mockEthAccount, .mockSolAccount, .mockFilAccount], allTokens: [BraveWallet.BlockchainToken] = [], transactions: [BraveWallet.TransactionInfo] = [], gasEstimation: BraveWallet.GasEstimation1559 = .init(), @@ -38,12 +40,14 @@ import Preferences ) -> TransactionConfirmationStore { let mockEthAssetPrice: BraveWallet.AssetPrice = .init(fromAsset: "eth", toAsset: "usd", price: "3059.99", assetTimeframeChange: "-57.23") let mockSolAssetPrice: BraveWallet.AssetPrice = .init(fromAsset: "sol", toAsset: "usd", price: "39.57", assetTimeframeChange: "-57.23") + let mockFilAssetPrice: BraveWallet.AssetPrice = .init(fromAsset: "fil", toAsset: "usd", price: "4.0", assetTimeframeChange: "-57.23") let formatter = WeiFormatter(decimalFormatStyle: .decimals(precision: 18)) let mockBalanceWei = formatter.weiString(from: 0.0896, radix: .hex, decimals: 18) ?? "" + let mockFILBalanceWei = formatter.weiString(from: 1, decimals: 18) ?? "" // setup test services let assetRatioService = BraveWallet.TestAssetRatioService() assetRatioService._price = { _, _, _, completion in - completion(true, [mockEthAssetPrice, mockSolAssetPrice]) + completion(true, [mockEthAssetPrice, mockSolAssetPrice, mockFilAssetPrice]) } let rpcService = BraveWallet.TestJsonRpcService() rpcService._chainIdForOrigin = { coin, origin, completion in @@ -55,8 +59,12 @@ import Preferences rpcService._allNetworks = { coin, completion in completion(allNetworksForCoinType[coin] ?? []) } - rpcService._balance = { _, _, _, completion in - completion(mockBalanceWei, .success, "") + rpcService._balance = { _, coin, _, completion in + if coin == .eth { + completion(mockBalanceWei, .success, "") + } else { // .fil + completion(mockFILBalanceWei, .success, "") + } } rpcService._erc20TokenAllowance = { _, _, _, _, completion in completion("16345785d8a0000", .success, "") // 0.1000 @@ -110,8 +118,10 @@ import Preferences accountInfos: []) if id == BraveWallet.KeyringId.default { keyring.accountInfos = accountInfos.filter { $0.coin == .eth } - } else { + } else if id == .solana { keyring.accountInfos = accountInfos.filter { $0.coin == .sol } + } else { + keyring.accountInfos = accountInfos.filter { $0.coin == .fil } } completion(keyring) } @@ -324,8 +334,10 @@ import Preferences solanaSendCopy.chainId = BraveWallet.SolanaMainnet let solanaSPLSendCopy = BraveWallet.TransactionInfo.previewConfirmedSolTokenTransfer.copy() as! BraveWallet.TransactionInfo solanaSPLSendCopy.chainId = BraveWallet.SolanaTestnet + let filecoinSendCopy = BraveWallet.TransactionInfo.mockFilUnapprovedSend.copy() as! BraveWallet.TransactionInfo + filecoinSendCopy.chainId = BraveWallet.FilecoinMainnet let pendingTransactions: [BraveWallet.TransactionInfo] = [ - sendCopy, swapCopy, solanaSendCopy, solanaSPLSendCopy + sendCopy, swapCopy, solanaSendCopy, solanaSPLSendCopy, filecoinSendCopy ].enumerated().map { (index, tx) in tx.txStatus = .unapproved // transactions sorted by created time, make sure they are in-order @@ -339,28 +351,30 @@ import Preferences ) let networkExpectation = expectation(description: "network-expectation") store.$network - .dropFirst(6) // `network` is assigned multiple times during setup - .collect(4) // collect all transactions + .dropFirst(7) // `network` is assigned multiple times during setup + .collect(5) // collect all transactions .sink { networks in defer { networkExpectation.fulfill() } - XCTAssertEqual(networks.count, 4) - XCTAssertEqual(networks[safe: 0], BraveWallet.NetworkInfo.mockSolanaTestnet) - XCTAssertEqual(networks[safe: 1], BraveWallet.NetworkInfo.mockSolana) - XCTAssertEqual(networks[safe: 2], BraveWallet.NetworkInfo.mockMainnet) - XCTAssertEqual(networks[safe: 3], BraveWallet.NetworkInfo.mockGoerli) + XCTAssertEqual(networks.count, 5) + XCTAssertEqual(networks[safe: 0], BraveWallet.NetworkInfo.mockFilecoinMainnet) + XCTAssertEqual(networks[safe: 1], BraveWallet.NetworkInfo.mockSolanaTestnet) + XCTAssertEqual(networks[safe: 2], BraveWallet.NetworkInfo.mockSolana) + XCTAssertEqual(networks[safe: 3], BraveWallet.NetworkInfo.mockMainnet) + XCTAssertEqual(networks[safe: 4], BraveWallet.NetworkInfo.mockGoerli) } .store(in: &cancellables) let activeTransactionIdExpectation = expectation(description: "activeTransactionId-expectation") store.$activeTransactionId .dropFirst() - .collect(4) // collect all transactions + .collect(5) // collect all transactions .sink { activeTransactionIds in defer { activeTransactionIdExpectation.fulfill() } - XCTAssertEqual(activeTransactionIds.count, 4) - XCTAssertEqual(activeTransactionIds[safe: 0], pendingTransactions[safe: 3]?.id) - XCTAssertEqual(activeTransactionIds[safe: 1], pendingTransactions[safe: 2]?.id) - XCTAssertEqual(activeTransactionIds[safe: 2], pendingTransactions[safe: 1]?.id) - XCTAssertEqual(activeTransactionIds[safe: 3], pendingTransactions[safe: 0]?.id) + XCTAssertEqual(activeTransactionIds.count, 5) + XCTAssertEqual(activeTransactionIds[safe: 0], pendingTransactions[safe: 4]?.id) + XCTAssertEqual(activeTransactionIds[safe: 1], pendingTransactions[safe: 3]?.id) + XCTAssertEqual(activeTransactionIds[safe: 2], pendingTransactions[safe: 2]?.id) + XCTAssertEqual(activeTransactionIds[safe: 3], pendingTransactions[safe: 1]?.id) + XCTAssertEqual(activeTransactionIds[safe: 4], pendingTransactions[safe: 0]?.id) } .store(in: &cancellables) @@ -368,6 +382,7 @@ import Preferences store.nextTransaction() // `swapCopy` on Ethereum Mainnet store.nextTransaction() // `solanaSendCopy` on Solana Mainnet store.nextTransaction() // `solanaSPLSendCopy` on Solana Testnet + store.nextTransaction() // `filecoinSendCopy` on filecoin mainnet await fulfillment(of: [networkExpectation, activeTransactionIdExpectation], timeout: 1) } @@ -495,6 +510,55 @@ import Preferences ) await fulfillment(of: [editExpectation], timeout: 1) } + + func testPrepareFilSend() async { + let mockAllTokens: [BraveWallet.BlockchainToken] = [.mockFilToken] + let mockTransaction: BraveWallet.TransactionInfo = .mockFilUnapprovedSend + let mockTransactions: [BraveWallet.TransactionInfo] = [mockTransaction].map { tx in + tx.txStatus = .unapproved + return tx + } + let store = setupStore( + accountInfos: [.mockFilAccount], + allTokens: mockAllTokens, + transactions: mockTransactions + ) + let prepareExpectation = expectation(description: "prepare") + await store.prepare() + store.$activeTransactionId + .sink { id in + defer { prepareExpectation.fulfill() } + XCTAssertEqual(id, mockTransaction.id) + } + .store(in: &cancellables) + store.$gasValue + .dropFirst() + .sink { value in + XCTAssertEqual(value, "0.000000155797727645") + } + .store(in: &cancellables) + store.$gasSymbol + .dropFirst() + .sink { value in + XCTAssertEqual(value, BraveWallet.BlockchainToken.mockFilToken.symbol) + } + .store(in: &cancellables) + store.$symbol + .dropFirst() + .sink { value in + XCTAssertEqual(value, BraveWallet.BlockchainToken.mockFilToken.symbol) + } + .store(in: &cancellables) + store.$value + .dropFirst() + .sink { value in + XCTAssertEqual(value, "1") + } + .store(in: &cancellables) + + await fulfillment(of: [prepareExpectation], timeout: 1) + + } } private extension BraveWallet.BlockchainToken { diff --git a/Tests/BraveWalletTests/TransactionParserTests.swift b/Tests/BraveWalletTests/TransactionParserTests.swift index 4379b2fd517..6ed2bd182ff 100644 --- a/Tests/BraveWalletTests/TransactionParserTests.swift +++ b/Tests/BraveWalletTests/TransactionParserTests.swift @@ -17,7 +17,7 @@ private extension BraveWallet.AccountInfo { self.init( accountId: .init( coin: coin, - keyringId: coin.keyringId, + keyringId: coin.keyringIds.first ?? .default, kind: .derived, address: address, bitcoinAccountIndex: 0, @@ -49,17 +49,28 @@ class TransactionParserTests: XCTestCase { $0.name = "Solana Account 2" $0.address = "0xeeeeeeeeeeffffffffff11111111112222222222" $0.accountId.address = "0xeeeeeeeeeeffffffffff11111111112222222222" + }, + (BraveWallet.AccountInfo.mockFilTestnetAccount.copy() as! BraveWallet.AccountInfo).then { + $0.name = "Filecoin Testnet 1" + $0.address = "fil_testnet_address_1" + $0.accountId.address = "fil_testnet_address_1" + }, + (BraveWallet.AccountInfo.mockFilTestnetAccount.copy() as! BraveWallet.AccountInfo).then { + $0.name = "Filecoin Testnet 2" + $0.address = "fil_testnet_address_2" + $0.accountId.address = "fil_testnet_address_2" } ] private let tokens: [BraveWallet.BlockchainToken] = [ - .previewToken, .previewDaiToken, .mockUSDCToken, .mockSolToken, .mockSpdToken, .mockSolanaNFTToken + .previewToken, .previewDaiToken, .mockUSDCToken, .mockSolToken, .mockSpdToken, .mockSolanaNFTToken, .mockFilToken ] let assetRatios: [String: Double] = [ "eth": 1, BraveWallet.BlockchainToken.previewDaiToken.assetRatioId.lowercased(): 2, BraveWallet.BlockchainToken.mockUSDCToken.assetRatioId.lowercased(): 3, "sol": 20, - BraveWallet.BlockchainToken.mockSpdToken.assetRatioId.lowercased(): 15 + BraveWallet.BlockchainToken.mockSpdToken.assetRatioId.lowercased(): 15, + "fil": 2 ] func testEthSendTransaction() { @@ -1090,4 +1101,100 @@ class TransactionParserTests: XCTestCase { ) XCTAssertNoDifference(expectedParsedCreateAccountWithSeed, TransactionParser.parseSolanaInstruction(createAccountWithSeedInstruction)) } + + func testFilecoinSendTransfer() { + let network: BraveWallet.NetworkInfo = .mockFilecoinTestnet + let fromAccount: BraveWallet.AccountInfo = (BraveWallet.AccountInfo.mockFilTestnetAccount.copy() as! BraveWallet.AccountInfo).then { + $0.address = "fil_testnet_address_1" + $0.name = "Filecoin Testnet 1" + } + let toAccount = (BraveWallet.AccountInfo.mockFilTestnetAccount.copy() as! BraveWallet.AccountInfo).then { + $0.address = "fil_testnet_address_2" + $0.name = "Filecoin Testnet 2" + } + + let transactionData: BraveWallet.FilTxData = .init( + nonce: "", + gasPremium: "100911", + gasFeeCap: "101965", + gasLimit: "1527953", + maxFee: "0", + to: toAccount.address, + value: "1000000000000000000" + ) + let transaction = BraveWallet.TransactionInfo( + id: "8", + fromAddress: fromAccount.address, + from: fromAccount.accountId, + txHash: "0xaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffggggg1234", + txDataUnion: .init(filTxData: transactionData), + txStatus: .unapproved, + txType: .other, + txParams: [], + txArgs: [ + ], + createdTime: Date(), + submittedTime: Date(), + confirmedTime: Date(), + originInfo: nil, + groupId: nil, + chainId: BraveWallet.FilecoinTestnet, + effectiveRecipient: nil + ) + let expectedParsedTransaction = ParsedTransaction( + transaction: transaction, + namedFromAddress: fromAccount.name, + fromAddress: fromAccount.address, + namedToAddress: toAccount.name, + toAddress: toAccount.address, + networkSymbol: "FIL", + details: .filSend( + .init( + sendToken: .mockFilToken, + sendValue: "1000000000000000000", + sendAmount: "1", + sendFiat: "$2.00", + gasPremium: "0.000000000000100911", + gasLimit: "0.000000000001527953", + gasFeeCap: "0.000000000000101965", + gasFee: GasFee( + fee: "0.000000155797727645", + fiat: "$0.0000003116" + ) + ) + ) + ) + + guard let parsedTransaction = TransactionParser.parseTransaction( + transaction: transaction, + network: network, + accountInfos: accountInfos, + visibleTokens: tokens, + allTokens: tokens, + assetRatios: assetRatios, + solEstimatedTxFee: nil, + currencyFormatter: currencyFormatter + ) else { + XCTFail("Failed to parse filecoinSendTransfer transaction") + return + } + + XCTAssertEqual(expectedParsedTransaction.fromAddress, parsedTransaction.fromAddress) + XCTAssertEqual(expectedParsedTransaction.namedFromAddress, parsedTransaction.namedFromAddress) + XCTAssertEqual(expectedParsedTransaction.toAddress, parsedTransaction.toAddress) + XCTAssertEqual(expectedParsedTransaction.networkSymbol, parsedTransaction.networkSymbol) + guard case let .filSend(expectedDetails) = expectedParsedTransaction.details, + case let .filSend(parsedDetails) = parsedTransaction.details else { + XCTFail("Incorrectly parsed solanaSystemTransfer transaction") + return + } + + XCTAssertEqual(expectedDetails.sendValue, parsedDetails.sendValue) + XCTAssertEqual(expectedDetails.sendAmount, parsedDetails.sendAmount) + XCTAssertEqual(expectedDetails.sendFiat, parsedDetails.sendFiat) + XCTAssertEqual(expectedDetails.gasPremium, parsedDetails.gasPremium) + XCTAssertEqual(expectedDetails.gasLimit, parsedDetails.gasLimit) + XCTAssertEqual(expectedDetails.gasFeeCap, parsedDetails.gasFeeCap) + XCTAssertEqual(expectedDetails.gasFee, parsedDetails.gasFee) + } } diff --git a/Tests/BraveWalletTests/TransactionsActivityStoreTests.swift b/Tests/BraveWalletTests/TransactionsActivityStoreTests.swift index 62309085c44..234de71cb26 100644 --- a/Tests/BraveWalletTests/TransactionsActivityStoreTests.swift +++ b/Tests/BraveWalletTests/TransactionsActivityStoreTests.swift @@ -22,7 +22,8 @@ class TransactionsActivityStoreTests: XCTestCase { let networks: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ .eth: [.mockMainnet], - .sol: [.mockSolana] + .sol: [.mockSolana], + .fil: [.mockFilecoinMainnet, .mockFilecoinTestnet] ] let visibleAssetsForCoins: [BraveWallet.CoinType: [BraveWallet.BlockchainToken]] = [ .eth: [ @@ -32,24 +33,36 @@ class TransactionsActivityStoreTests: XCTestCase { .sol: [ BraveWallet.NetworkInfo.mockSolana.nativeToken.copy(asVisibleAsset: true), .mockSolanaNFTToken.copy(asVisibleAsset: true), - .mockSpdToken.copy(asVisibleAsset: true)] + .mockSpdToken.copy(asVisibleAsset: true)], + .fil: [ + BraveWallet.NetworkInfo.mockFilecoinMainnet.nativeToken.copy(asVisibleAsset: true), + BraveWallet.NetworkInfo.mockFilecoinTestnet.nativeToken.copy(asVisibleAsset: true)] ] let tokenRegistry: [BraveWallet.CoinType: [BraveWallet.BlockchainToken]] = [:] let mockAssetPrices: [BraveWallet.AssetPrice] = [ .init(fromAsset: "eth", toAsset: "usd", price: "3059.99", assetTimeframeChange: "-57.23"), .init(fromAsset: "usdc", toAsset: "usd", price: "1.00", assetTimeframeChange: "-57.23"), .init(fromAsset: "sol", toAsset: "usd", price: "2.00", assetTimeframeChange: "-57.23"), - .init(fromAsset: "spd", toAsset: "usd", price: "0.50", assetTimeframeChange: "-57.23") + .init(fromAsset: "spd", toAsset: "usd", price: "0.50", assetTimeframeChange: "-57.23"), + .init(fromAsset: BraveWallet.BlockchainToken.mockFilToken.assetRatioId.lowercased(), + toAsset: "usd", price: "4.00", assetTimeframeChange: "-57.23") ] func testUpdate() { let keyringService = BraveWallet.TestKeyringService() keyringService._addObserver = { _ in } keyringService._keyringInfo = { keyringId, completion in - if keyringId == BraveWallet.KeyringId.default { + switch keyringId { + case .default: completion(.mockDefaultKeyringInfo) - } else { + case .solana: completion(.mockSolanaKeyringInfo) + case .filecoin: + completion(.mockFilecoinKeyringInfo) + case .filecoinTestnet: + completion(.mockFilecoinTestnetKeyringInfo) + default: + completion(.mockDefaultKeyringInfo) } } @@ -57,8 +70,10 @@ class TransactionsActivityStoreTests: XCTestCase { rpcService._allNetworks = { coin, completion in if coin == .sol { completion([.mockSolana, .mockSolanaTestnet]) - } else { + } else if coin == .eth { completion([.mockMainnet, .mockGoerli]) + } else { // .fil + completion([.mockFilecoinMainnet, .mockFilecoinTestnet]) } } @@ -83,7 +98,10 @@ class TransactionsActivityStoreTests: XCTestCase { let solSendTxCopy = BraveWallet.TransactionInfo.previewConfirmedSolSystemTransfer.copy() as! BraveWallet.TransactionInfo // default in mainnet let solTestnetSendTxCopy = BraveWallet.TransactionInfo.previewConfirmedSolTokenTransfer.copy() as! BraveWallet.TransactionInfo solTestnetSendTxCopy.chainId = BraveWallet.SolanaTestnet - let mockTxs: [BraveWallet.TransactionInfo] = [ethSendTxCopy, goerliSwapTxCopy, solSendTxCopy, solTestnetSendTxCopy].enumerated().map { (index, tx) in + let filSendTxCopy = BraveWallet.TransactionInfo.mockFilUnapprovedSend.copy() as! BraveWallet.TransactionInfo + let filTestnetSendTxCopy = BraveWallet.TransactionInfo.mockFilUnapprovedSend.copy() as! BraveWallet.TransactionInfo + filTestnetSendTxCopy.chainId = BraveWallet.FilecoinTestnet + let mockTxs: [BraveWallet.TransactionInfo] = [ethSendTxCopy, goerliSwapTxCopy, solSendTxCopy, solTestnetSendTxCopy, filSendTxCopy, filTestnetSendTxCopy].enumerated().map { (index, tx) in tx.txStatus = .unapproved // transactions sorted by created time, make sure they are in-order tx.createdTime = firstTransactionDate.addingTimeInterval(TimeInterval(index)) @@ -95,8 +113,10 @@ class TransactionsActivityStoreTests: XCTestCase { txService._allTransactionInfo = { coin, chainId, address, completion in if coin == .eth { completion([ethSendTxCopy, goerliSwapTxCopy].filter({ $0.chainId == chainId })) - } else { + } else if coin == .sol { completion([solSendTxCopy, solTestnetSendTxCopy].filter({ $0.chainId == chainId })) + } else { // .fil + completion([filSendTxCopy, filTestnetSendTxCopy].filter({ $0.chainId == chainId })) } } @@ -144,28 +164,37 @@ class TransactionsActivityStoreTests: XCTestCase { XCTAssertEqual(transactionSummariesWithoutPrices.map(\.txInfo.txHash), expectedSortedOrder.map(\.txHash)) XCTAssertEqual(transactionSummariesWithPrices.map(\.txInfo.txHash), expectedSortedOrder.map(\.txHash)) // verify they are populated with correct tx (summaries are tested in `TransactionParserTests`) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 0]?.txInfo, filTestnetSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 0]?.txInfo.chainId, filTestnetSendTxCopy.chainId) + XCTAssertEqual(transactionSummariesWithPrices[safe: 0]?.txInfo, filTestnetSendTxCopy) + + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 1]?.txInfo, filSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 1]?.txInfo.chainId, filSendTxCopy.chainId) + XCTAssertEqual(transactionSummariesWithPrices[safe: 1]?.txInfo, filSendTxCopy) + + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 2]?.txInfo, solTestnetSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 2]?.txInfo.chainId, solTestnetSendTxCopy.chainId) + XCTAssertEqual(transactionSummariesWithPrices[safe: 2]?.txInfo, solTestnetSendTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 0]?.txInfo, solTestnetSendTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 0]?.txInfo.chainId, solTestnetSendTxCopy.chainId) - XCTAssertEqual(transactionSummariesWithPrices[safe: 0]?.txInfo, solTestnetSendTxCopy) - - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 1]?.txInfo, solSendTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 1]?.txInfo.chainId, solSendTxCopy.chainId) - XCTAssertEqual(transactionSummariesWithPrices[safe: 1]?.txInfo, solSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 3]?.txInfo, solSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 3]?.txInfo.chainId, solSendTxCopy.chainId) + XCTAssertEqual(transactionSummariesWithPrices[safe: 3]?.txInfo, solSendTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 2]?.txInfo, goerliSwapTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 2]?.txInfo.chainId, goerliSwapTxCopy.chainId) - XCTAssertEqual(transactionSummariesWithPrices[safe: 2]?.txInfo, goerliSwapTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 4]?.txInfo, goerliSwapTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 4]?.txInfo.chainId, goerliSwapTxCopy.chainId) + XCTAssertEqual(transactionSummariesWithPrices[safe: 4]?.txInfo, goerliSwapTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 3]?.txInfo, ethSendTxCopy) - XCTAssertEqual(transactionSummariesWithoutPrices[safe: 3]?.txInfo.chainId, ethSendTxCopy.chainId) - XCTAssertEqual(transactionSummariesWithPrices[safe: 3]?.txInfo, ethSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 5]?.txInfo, ethSendTxCopy) + XCTAssertEqual(transactionSummariesWithoutPrices[safe: 5]?.txInfo.chainId, ethSendTxCopy.chainId) + XCTAssertEqual(transactionSummariesWithPrices[safe: 5]?.txInfo, ethSendTxCopy) // verify gas fee fiat - XCTAssertEqual(transactionSummariesWithPrices[safe: 0]?.gasFee?.fiat, "$0.000000002") - XCTAssertEqual(transactionSummariesWithPrices[safe: 1]?.gasFee?.fiat, "$0.000000002") - XCTAssertEqual(transactionSummariesWithPrices[safe: 2]?.gasFee?.fiat, "$255.03792654") - XCTAssertEqual(transactionSummariesWithPrices[safe: 3]?.gasFee?.fiat, "$10.41008598" ) + XCTAssertEqual(transactionSummariesWithPrices[safe: 0]?.gasFee?.fiat, "$0.0000006232") + XCTAssertEqual(transactionSummariesWithPrices[safe: 1]?.gasFee?.fiat, "$0.0000006232") + XCTAssertEqual(transactionSummariesWithPrices[safe: 2]?.gasFee?.fiat, "$0.000000002") + XCTAssertEqual(transactionSummariesWithPrices[safe: 3]?.gasFee?.fiat, "$0.000000002") + XCTAssertEqual(transactionSummariesWithPrices[safe: 4]?.gasFee?.fiat, "$255.03792654") + XCTAssertEqual(transactionSummariesWithPrices[safe: 5]?.gasFee?.fiat, "$10.41008598" ) } .store(in: &cancellables) diff --git a/Tests/BraveWalletTests/UserAssetsStoreTests.swift b/Tests/BraveWalletTests/UserAssetsStoreTests.swift index 6e849c3f778..95e652279d2 100644 --- a/Tests/BraveWalletTests/UserAssetsStoreTests.swift +++ b/Tests/BraveWalletTests/UserAssetsStoreTests.swift @@ -15,11 +15,13 @@ class UserAssetsStoreTests: XCTestCase { let networks: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [ .eth: [.mockMainnet], - .sol: [.mockSolana] + .sol: [.mockSolana], + .fil: [.mockFilecoinMainnet] ] let tokenRegistry: [BraveWallet.CoinType: [BraveWallet.BlockchainToken]] = [ .eth: [.mockUSDCToken], - .sol: [.mockSpdToken] + .sol: [.mockSpdToken], + .fil: [.mockFilToken] ] private func setupServices() -> (BraveWallet.TestKeyringService, BraveWallet.TestJsonRpcService, BraveWallet.TestBlockchainRegistry, BraveWallet.TestAssetRatioService) { @@ -73,6 +75,15 @@ class UserAssetsStoreTests: XCTestCase { ], sortOrder: 1) ) + } else if network.chainId == BraveWallet.FilecoinMainnet { + result.append( + NetworkAssets( + network: .mockFilecoinMainnet, + tokens: [ + BraveWallet.NetworkInfo.mockFilecoinMainnet.nativeToken.copy(asVisibleAsset: true) + ], + sortOrder: 1) + ) } } return result @@ -92,7 +103,7 @@ class UserAssetsStoreTests: XCTestCase { .dropFirst() .sink { assetStores in defer { assetStoresException.fulfill() } - XCTAssertEqual(assetStores.count, 6) + XCTAssertEqual(assetStores.count, 7) XCTAssertEqual(assetStores[0].token.symbol, BraveWallet.NetworkInfo.mockSolana.nativeToken.symbol) XCTAssertTrue(assetStores[0].token.visible) @@ -117,6 +128,10 @@ class UserAssetsStoreTests: XCTestCase { XCTAssertEqual(assetStores[5].token.symbol, BraveWallet.BlockchainToken.mockUSDCToken.symbol) XCTAssertFalse(assetStores[5].token.visible) XCTAssertEqual(assetStores[5].network, BraveWallet.NetworkInfo.mockMainnet) + + XCTAssertEqual(assetStores[6].token.symbol, BraveWallet.BlockchainToken.mockFilToken.symbol) + XCTAssertTrue(assetStores[6].token.visible) + XCTAssertEqual(assetStores[6].network, BraveWallet.NetworkInfo.mockFilecoinMainnet) } .store(in: &cancellables) @@ -156,6 +171,15 @@ class UserAssetsStoreTests: XCTestCase { ], sortOrder: 1) ) + } else if network.chainId == BraveWallet.FilecoinMainnet { + result.append( + NetworkAssets( + network: .mockFilecoinMainnet, + tokens: [ + BraveWallet.NetworkInfo.mockFilecoinMainnet.nativeToken.copy(asVisibleAsset: true) + ], + sortOrder: 1) + ) } } return result