From 883a39feda833d08a48a172d39d10dc4d0f63cb7 Mon Sep 17 00:00:00 2001 From: Nuo Xu Date: Wed, 15 Nov 2023 19:06:58 +0800 Subject: [PATCH] address or comments. --- .../Crypto/NFT/NFTDetailView.swift | 46 ++++++------- Sources/BraveWallet/Crypto/NFT/NFTView.swift | 1 + .../Crypto/Search/AssetSearchView.swift | 1 + .../Crypto/Stores/CryptoStore.swift | 1 + .../Crypto/Stores/NFTDetailStore.swift | 37 ++++++++++- .../BraveWallet/Crypto/Stores/NFTStore.swift | 66 ++++++++++--------- .../Extensions/BraveWalletExtensions.swift | 56 ++++++++++++++-- Sources/BraveWallet/WalletStrings.swift | 2 +- 8 files changed, 144 insertions(+), 66 deletions(-) diff --git a/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift b/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift index 933863d6067..07b28ea7f50 100644 --- a/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift +++ b/Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift @@ -10,6 +10,7 @@ import BraveUI import SDWebImageSwiftUI struct NFTDetailView: View { + @ObservedObject var keyringStore: KeyringStore @ObservedObject var nftDetailStore: NFTDetailStore @Binding var buySendSwapDestination: BuySendSwapDestination? var onNFTMetadataRefreshed: ((NFTMetadata) -> Void)? @@ -95,7 +96,7 @@ struct NFTDetailView: View { .offset(y: 16) } VStack(alignment: .leading, spacing: 8) { - Text(nftDetailStore.nft.nftTokenTitle) + Text(nftDetailStore.nft.nftDetailTitle) .font(.title3.weight(.semibold)) .foregroundColor(Color(.braveLabel)) Text(nftDetailStore.nft.name) @@ -130,35 +131,21 @@ struct NFTDetailView: View { NFTDetailRow(title: nftDetailStore.nft.isErc721 ? Strings.Wallet.contractAddressAccessibilityLabel : Strings.Wallet.tokenMintAddress) { Button { if nftDetailStore.nft.isErc721 { - if let explorerURL = nftDetailStore.networkInfo.blockExplorerUrls.first { - let baseURL = "\(explorerURL)/token/\(nftDetailStore.nft.contractAddress)" - var nftURL = URL(string: baseURL) - if let tokenId = Int(nftDetailStore.nft.tokenId.removingHexPrefix, radix: 16) { - nftURL = URL(string: "\(baseURL)?a=\(tokenId)") - } - - if let url = nftURL { - openWalletURL(url) - } + if let url = nftDetailStore.networkInfo.erc721TokenBlockExplorerURL(nftDetailStore.nft) { + openWalletURL(url) } } else { - if WalletConstants.supportedTestNetworkChainIds.contains(nftDetailStore.networkInfo.chainId) { - if let components = nftDetailStore.networkInfo.blockExplorerUrls.first?.separatedBy("/?cluster="), let baseURL = components.first { - let cluster = components.last ?? "" - if let nftURL = URL(string: "\(baseURL)/address/\(nftDetailStore.nft.contractAddress)/?cluster=\(cluster)") { - openWalletURL(nftURL) - } - } - } else { - if let explorerURL = nftDetailStore.networkInfo.blockExplorerUrls.first, let nftURL = URL(string: "\(explorerURL)/address/\(nftDetailStore.nft.contractAddress)") { - openWalletURL(nftURL) - } + if let url = nftDetailStore.networkInfo.splTokenBlockExplorerURL(nftDetailStore.nft) { + openWalletURL(url) } } } label: { - Text(nftDetailStore.nft.contractAddress.truncatedAddress) - .font(.subheadline) - .foregroundColor(Color(.braveBlurpleTint)) + HStack { + Text(nftDetailStore.nft.contractAddress.truncatedAddress) + Image(systemName: "arrow.up.forward.square") + } + .font(.subheadline) + .foregroundColor(Color(.braveBlurpleTint)) } } NFTDetailRow(title: Strings.Wallet.nftDetailBlockchain) { @@ -212,11 +199,17 @@ struct NFTDetailView: View { onNFTMetadataRefreshed?(newMetadata) } }) + .onChange(of: keyringStore.isWalletLocked, perform: { isLocked in + guard isLocked else { return } + if isPresentingRemoveAlert { + isPresentingRemoveAlert = false + } + }) .onAppear { nftDetailStore.update() } .background(Color(UIColor.braveGroupedBackground).ignoresSafeArea()) - .navigationBarTitle(nftDetailStore.nft.nftTokenTitle) + .navigationBarTitle(nftDetailStore.nft.nftDetailTitle) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Menu { @@ -291,6 +284,7 @@ struct NFTDetailView: View { .font(.footnote) .foregroundStyle(Color(.secondaryBraveLabel)) } + .padding(.bottom, 28) }) ) } diff --git a/Sources/BraveWallet/Crypto/NFT/NFTView.swift b/Sources/BraveWallet/Crypto/NFT/NFTView.swift index 61859c5fec7..80bb7a19d2c 100644 --- a/Sources/BraveWallet/Crypto/NFT/NFTView.swift +++ b/Sources/BraveWallet/Crypto/NFT/NFTView.swift @@ -301,6 +301,7 @@ struct NFTView: View { destination: { if let nftViewModel = selectedNFTViewModel { NFTDetailView( + keyringStore: keyringStore, nftDetailStore: cryptoStore.nftDetailStore(for: nftViewModel.token, nftMetadata: nftViewModel.nftMetadata, owner: nftStore.owner(for: nftViewModel.token)), buySendSwapDestination: buySendSwapDestination, onNFTMetadataRefreshed: { nftMetadata in diff --git a/Sources/BraveWallet/Crypto/Search/AssetSearchView.swift b/Sources/BraveWallet/Crypto/Search/AssetSearchView.swift index 8362c831964..cf34af6c7e8 100644 --- a/Sources/BraveWallet/Crypto/Search/AssetSearchView.swift +++ b/Sources/BraveWallet/Crypto/Search/AssetSearchView.swift @@ -139,6 +139,7 @@ struct AssetSearchView: View { if let selectedToken { if selectedToken.isErc721 { NFTDetailView( + keyringStore: keyringStore, nftDetailStore: cryptoStore.nftDetailStore(for: selectedToken, nftMetadata: allNFTMetadata[selectedToken.id], owner: nil), buySendSwapDestination: .constant(nil) ) { metadata in diff --git a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift index 0b0e4c48aa2..1a6a72194f6 100644 --- a/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/CryptoStore.swift @@ -516,6 +516,7 @@ public class CryptoStore: ObservableObject, WalletObserverStore { } let store = NFTDetailStore( assetManager: userAssetManager, + keyringService: keyringService, rpcService: rpcService, ipfsApi: ipfsApi, nft: nft, diff --git a/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift b/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift index c46b2dbf192..d26a7f0fee5 100644 --- a/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NFTDetailStore.swift @@ -67,9 +67,10 @@ struct NFTAttribute: Codable, Equatable, Identifiable { class NFTDetailStore: ObservableObject, WalletObserverStore { private let assetManager: WalletUserAssetManagerType + private let keyringService: BraveWalletKeyringService private let rpcService: BraveWalletJsonRpcService private let ipfsApi: IpfsAPI - let owner: BraveWallet.AccountInfo? + @Published var owner: BraveWallet.AccountInfo? @Published var nft: BraveWallet.BlockchainToken @Published var isLoading: Bool = false @Published var nftMetadata: NFTMetadata? @@ -79,6 +80,7 @@ class NFTDetailStore: ObservableObject, WalletObserverStore { init( assetManager: WalletUserAssetManagerType, + keyringService: BraveWalletKeyringService, rpcService: BraveWalletJsonRpcService, ipfsApi: IpfsAPI, nft: BraveWallet.BlockchainToken, @@ -86,6 +88,7 @@ class NFTDetailStore: ObservableObject, WalletObserverStore { owner: BraveWallet.AccountInfo? ) { self.assetManager = assetManager + self.keyringService = keyringService self.rpcService = rpcService self.ipfsApi = ipfsApi self.nft = nft @@ -100,6 +103,38 @@ class NFTDetailStore: ObservableObject, WalletObserverStore { networkInfo = network } + if owner == nil { + let accounts = await keyringService.allAccounts().accounts + let nftBalances: [String: Int] = await withTaskGroup( + of: [String: Int].self, + body: { @MainActor [rpcService, networkInfo, nft] group in + for account in accounts where account.coin == nft.coin { + group.addTask { @MainActor in + let balanceForToken = await rpcService.balance( + for: nft, + in: account, + network: networkInfo + ) + return [account.address: Int(balanceForToken ?? 0)] + } + } + return await group.reduce(into: [String: Int](), { partialResult, new in + for key in new.keys { + partialResult[key] = new[key] + } + }) + } + ) + if let address = nftBalances.first(where: { address, balance in + balance > 0 + })?.key, + let account = accounts.first(where: { accountInfo in + accountInfo.address.caseInsensitiveCompare(address) == .orderedSame + }) { + owner = account + } + } + if nftMetadata == nil { isLoading = true nftMetadata = await rpcService.fetchNFTMetadata(for: nft, ipfsApi: self.ipfsApi) diff --git a/Sources/BraveWallet/Crypto/Stores/NFTStore.swift b/Sources/BraveWallet/Crypto/Stores/NFTStore.swift index 2c2f5794269..581ca625ce7 100644 --- a/Sources/BraveWallet/Crypto/Stores/NFTStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/NFTStore.swift @@ -303,42 +303,44 @@ public class NFTStore: ObservableObject, WalletObserverStore { for networkAssets in [userVisibleNFTs, userHiddenNFTs, unionedSpamNFTs] { allNFTs.append(contentsOf: networkAssets.flatMap(\.tokens)) } - // fetch balance for all NFTs - let allAccounts = filters.accounts.map(\.model) - nftBalancesCache = await withTaskGroup( - of: [String: [String: Int]].self, - body: { @MainActor [nftBalancesCache, rpcService] group in - for nft in allNFTs { // for each NFT - guard let networkForNFT = allNetworks.first(where: { $0.chainId == nft.chainId }) else { - continue - } - group.addTask { @MainActor in - let updatedBalances = await withTaskGroup( - of: [String: Int].self, - body: { @MainActor group in - for account in allAccounts where account.coin == nft.coin { - group.addTask { @MainActor in - let balanceForToken = await rpcService.balance( - for: nft, - in: account, - network: networkForNFT - ) - return [account.address: Int(balanceForToken ?? 0)] + // if we're not hiding unowned or grouping by account, balance isn't needed + if filters.isHidingUnownedNFTs { + let allAccounts = filters.accounts.map(\.model) + nftBalancesCache = await withTaskGroup( + of: [String: [String: Int]].self, + body: { @MainActor [nftBalancesCache, rpcService] group in + for nft in allNFTs { // for each NFT + guard let networkForNFT = allNetworks.first(where: { $0.chainId == nft.chainId }) else { + continue + } + group.addTask { @MainActor in + let updatedBalances = await withTaskGroup( + of: [String: Int].self, + body: { @MainActor group in + for account in allAccounts where account.coin == nft.coin { + group.addTask { @MainActor in + let balanceForToken = await rpcService.balance( + for: nft, + in: account, + network: networkForNFT + ) + return [account.address: Int(balanceForToken ?? 0)] + } } - } - return await group.reduce(into: [String: Int](), { partialResult, new in - partialResult.merge(with: new) + return await group.reduce(into: [String: Int](), { partialResult, new in + partialResult.merge(with: new) + }) }) - }) - var tokenBalances = nftBalancesCache[nft.id] ?? [:] - tokenBalances.merge(with: updatedBalances) - return [nft.id: tokenBalances] + var tokenBalances = nftBalancesCache[nft.id] ?? [:] + tokenBalances.merge(with: updatedBalances) + return [nft.id: tokenBalances] + } } - } - return await group.reduce(into: [String: [String: Int]](), { partialResult, new in - partialResult.merge(with: new) + return await group.reduce(into: [String: [String: Int]](), { partialResult, new in + partialResult.merge(with: new) + }) }) - }) + } guard !Task.isCancelled else { return } userNFTGroups = buildNFTGroupModels( diff --git a/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift b/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift index 010a350a145..29dd2b46f6a 100644 --- a/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift +++ b/Sources/BraveWallet/Extensions/BraveWalletExtensions.swift @@ -121,6 +121,8 @@ extension BraveWallet.CoinType { return [.filecoin, .filecoinTestnet] case .btc: return [.bitcoin84, .bitcoin84Testnet] + case .zec: + return [.zCashMainnet, .zCashTestnet] @unknown default: return [.default] } @@ -134,7 +136,7 @@ extension BraveWallet.CoinType { return Strings.Wallet.coinTypeSolana case .fil: return Strings.Wallet.coinTypeFilecoin - case .btc: + case .btc, .zec: fallthrough @unknown default: return Strings.Wallet.coinTypeUnknown @@ -149,7 +151,7 @@ extension BraveWallet.CoinType { return Strings.Wallet.coinTypeSolanaDescription case .fil: return Strings.Wallet.coinTypeFilecoinDescription - case .btc: + case .btc, .zec: fallthrough @unknown default: return Strings.Wallet.coinTypeUnknown @@ -164,7 +166,7 @@ extension BraveWallet.CoinType { return "sol-asset-icon" case .fil: return "filecoin-asset-icon" - case .btc: + case .btc, .zec: fallthrough @unknown default: return "" @@ -179,7 +181,7 @@ extension BraveWallet.CoinType { return Strings.Wallet.defaultSolAccountName case .fil: return Strings.Wallet.defaultFilAccountName - case .btc: + case .btc, .zec: fallthrough @unknown default: return "" @@ -194,7 +196,7 @@ extension BraveWallet.CoinType { return Strings.Wallet.defaultSecondarySolAccountName case .fil: return Strings.Wallet.defaultSecondaryFilAccountName - case .btc: + case .btc, .zec: fallthrough @unknown default: return "" @@ -210,7 +212,7 @@ extension BraveWallet.CoinType { return 2 case .fil: return 3 - case .btc: + case .btc, .zec: fallthrough @unknown default: return 10 @@ -259,6 +261,38 @@ extension BraveWallet.NetworkInfo { } return nil } + + /// Generate the explorer link for the given ERC721 token with the current NetworkInfo + func erc721TokenBlockExplorerURL(_ token: BraveWallet.BlockchainToken) -> URL? { + guard token.isErc721, + let explorerURL = blockExplorerUrls.first + else { return nil } + + let baseURL = "\(explorerURL)/token/\(token.contractAddress)" + var tokenURL = URL(string: baseURL) + if let tokenId = Int(token.tokenId.removingHexPrefix, radix: 16) { + tokenURL = URL(string: "\(baseURL)?a=\(tokenId)") + } + return tokenURL + } + + /// Generate the explorer link for the given SPL token with the current NetworkInfo + func splTokenBlockExplorerURL(_ token: BraveWallet.BlockchainToken) -> URL? { + guard !token.isErc721, !token.isErc20, !token.isErc1155 else { return nil } + if WalletConstants.supportedTestNetworkChainIds.contains(chainId) { + if let components = blockExplorerUrls.first?.separatedBy("/?cluster="), let baseURL = components.first { + let cluster = components.last ?? "" + if let tokenURL = URL(string: "\(baseURL)/address/\(token.contractAddress)/?cluster=\(cluster)") { + return tokenURL + } + } + } else { + if let explorerURL = blockExplorerUrls.first, let tokenURL = URL(string: "\(explorerURL)/address/\(token.contractAddress)") { + return tokenURL + } + } + return nil + } } extension BraveWallet.BlockchainToken { @@ -287,6 +321,14 @@ extension BraveWallet.BlockchainToken { } var nftTokenTitle: String { + if isErc721, let tokenId = Int(tokenId.removingHexPrefix, radix: 16) { + return "\(name) #\(tokenId)" + } else { + return name + } + } + + var nftDetailTitle: String { if isErc721, let tokenId = Int(tokenId.removingHexPrefix, radix: 16) { return "\(symbol) #\(tokenId)" } else { @@ -419,6 +461,8 @@ extension BraveWallet.KeyringId { return chainId == BraveWallet.FilecoinMainnet ? .filecoin : .filecoinTestnet case.btc: return chainId == BraveWallet.BitcoinMainnet ? .bitcoin84 : .bitcoin84Testnet + case .zec: + return chainId == BraveWallet.ZCashMainnet ? .zCashMainnet: .zCashTestnet @unknown default: return .default } diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index b0601c50c49..163a35cf177 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -3765,7 +3765,7 @@ extension Strings { "wallet.nftDetailOwnedBy", tableName: "BraveWallet", bundle: .module, - value: "Owned by", + value: "Owned By", comment: "The title of the row under `Overview` section in NFT details screen. When this NFT has an owner." ) public static let signTransactionSignRisk = NSLocalizedString(