diff --git a/Maskbook.xcodeproj/project.pbxproj b/Maskbook.xcodeproj/project.pbxproj index dc904e3a..fd655a9c 100644 --- a/Maskbook.xcodeproj/project.pbxproj +++ b/Maskbook.xcodeproj/project.pbxproj @@ -772,6 +772,7 @@ 6F022CD428A4CE3900B74297 /* SearchSingleNFTViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F022CD328A4CE3900B74297 /* SearchSingleNFTViewController.swift */; }; 6F022CD628A4CF9B00B74297 /* SearchSingleNFTViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F022CD528A4CF9B00B74297 /* SearchSingleNFTViewModel.swift */; }; 6F022CD828A4CFBD00B74297 /* SearchSingleNFTCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F022CD728A4CFBD00B74297 /* SearchSingleNFTCollectionViewCell.swift */; }; + 6F022CDA28A93B1800B74297 /* UnlockNFTViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F022CD928A93B1800B74297 /* UnlockNFTViewController.swift */; }; 6F07ABE1279022B80039D69D /* NFTCollectionStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F07ABE0279022B80039D69D /* NFTCollectionStatusModel.swift */; }; 6F07ABE3279027E70039D69D /* NFTAssetPriceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F07ABE2279027E70039D69D /* NFTAssetPriceModel.swift */; }; 6F07ABE5279697450039D69D /* NFTCollectionDetailTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F07ABE4279697450039D69D /* NFTCollectionDetailTableViewCell.swift */; }; @@ -1655,6 +1656,7 @@ 6F022CD328A4CE3900B74297 /* SearchSingleNFTViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSingleNFTViewController.swift; sourceTree = ""; }; 6F022CD528A4CF9B00B74297 /* SearchSingleNFTViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSingleNFTViewModel.swift; sourceTree = ""; }; 6F022CD728A4CFBD00B74297 /* SearchSingleNFTCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSingleNFTCollectionViewCell.swift; sourceTree = ""; }; + 6F022CD928A93B1800B74297 /* UnlockNFTViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockNFTViewController.swift; sourceTree = ""; }; 6F07ABE0279022B80039D69D /* NFTCollectionStatusModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFTCollectionStatusModel.swift; sourceTree = ""; }; 6F07ABE2279027E70039D69D /* NFTAssetPriceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTAssetPriceModel.swift; sourceTree = ""; }; 6F07ABE4279697450039D69D /* NFTCollectionDetailTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTCollectionDetailTableViewCell.swift; sourceTree = ""; }; @@ -2061,6 +2063,7 @@ 1F57023927A27E4900C96909 /* RedPacketShareSheet.swift */, 1F44794B288A5D8F0098BAE9 /* NftRedPacketShareViewController.swift */, 1F3B23BB28A50B510015949E /* NFTLuckyDropCreationFlow.swift */, + 6F022CD928A93B1800B74297 /* UnlockNFTViewController.swift */, ); path = Controller; sourceTree = ""; @@ -4891,6 +4894,7 @@ 2A6ABBB82720053B00CC3AED /* RemoteBackupActionsViewController.swift in Sources */, 2AD856002812875A0064DDA5 /* MaskWrapper.swift in Sources */, 2DA3FAB4273CDFBA0013A784 /* ChainInfo.swift in Sources */, + 6F022CDA28A93B1800B74297 /* UnlockNFTViewController.swift in Sources */, 2D6677FA27688FBE00528F4D /* WCRequestsHandler.swift in Sources */, 5DC018422859BE84004973CA /* ArweaveHttpClient.swift in Sources */, 0F2051B3265E3B2F00602A0C /* DerivationPath.swift in Sources */, diff --git a/Maskbook/Scene/App/RedPacket/Controller/LuckyDropViewController.swift b/Maskbook/Scene/App/RedPacket/Controller/LuckyDropViewController.swift index 956c5b88..8f2dd371 100644 --- a/Maskbook/Scene/App/RedPacket/Controller/LuckyDropViewController.swift +++ b/Maskbook/Scene/App/RedPacket/Controller/LuckyDropViewController.swift @@ -66,7 +66,7 @@ class LuckyDropViewController: BaseViewController { case let .addCollectibles(groupName, contractAddress, selectedIdentifiers): self.coordinator.present( - scene: .luckydropSearchNFT(delegate: self.viewModel.nftViewModel, contractAddress: contractAddress), + scene: .luckydropSearchNFT(delegate: self.viewModel.nftViewModel, contractAddress: contractAddress, selectedIdentifiers: selectedIdentifiers), transition: .detail() ) @@ -83,8 +83,11 @@ class LuckyDropViewController: BaseViewController { case .unlockWallet: self.unlockWallet() - case let .unlockNFT(contractAddress, gasItem): - // TODO: unlock nft permission + case let .unlockNFT(groupInfo, gasItem, gasFeeViewModel): + self.coordinator.present(scene: .unlockNFT(gasFeeViewModel: gasFeeViewModel, gasFeeItem: gasItem, groupInfo: groupInfo, completion: { hash, error in + + }), transition: .popup) + break } } diff --git a/Maskbook/Scene/App/RedPacket/Controller/UnlockNFTViewController.swift b/Maskbook/Scene/App/RedPacket/Controller/UnlockNFTViewController.swift new file mode 100644 index 00000000..5e1bb068 --- /dev/null +++ b/Maskbook/Scene/App/RedPacket/Controller/UnlockNFTViewController.swift @@ -0,0 +1,64 @@ +// +// UnlockNFTViewController.swift +// Maskbook +// +// Created by caiyu on 2022/8/14. +// Copyright © 2022 dimension. All rights reserved. +// + +import Combine +import CoreDataStack +import SwiftUI +import UIKit +import web3swift + +final class UnlockNFTViewController: SheetViewController { + private(set) var disposeBag: Set = [] + + @InjectedProvider(\.mainCoordinator) + private var mainCoordinator + + private var viewModel: UnlockNFTViewModel + + init( + gasFeeViewModel: GasFeeViewModel, + gasFeeItem: GasFeeCellItem, + groupInfo: CollectiableGroup, + completion: ((String?, Error?) -> Void)? + ) { + viewModel = UnlockNFTViewModel(gasFeeViewModel: gasFeeViewModel, gasFeeItem: gasFeeItem, collectibleGroup: groupInfo, completion: completion) + + super.init(presenter: SheetPresenter( + presentStyle: .translucent, + transition: KeyboardSheetTransition() + ) + ) + dismissAction = { [weak self] in + self?.viewModel.completion?(nil, UnlockNFTError.cancel) + } + } + + override func buildContent() { + super.buildContent() + + UnlockNFTView(viewModel: viewModel).asSheetContent(in: self) + } + + override func buildEvent() { + super.buildEvent() + + viewModel + .$buttonState + .map { state in + state != .sending + } + .assign(to: \.dissmissOnTap, on: self) + .store(in: &disposeBag) + } +} + +extension UnlockNFTViewController { + enum UnlockNFTError: Error { + case cancel + } +} diff --git a/Maskbook/Scene/App/RedPacket/View/UnlockNFTView.swift b/Maskbook/Scene/App/RedPacket/View/UnlockNFTView.swift index b14b2c02..c0c2d5b4 100644 --- a/Maskbook/Scene/App/RedPacket/View/UnlockNFTView.swift +++ b/Maskbook/Scene/App/RedPacket/View/UnlockNFTView.swift @@ -11,14 +11,14 @@ import SwiftUI struct UnlockNFTView: View { @ObservedObject var viewModel: UnlockNFTViewModel @InjectedProvider(\.mainCoordinator) private var mainCoordinator - + var tipsView: some View { HStack(spacing: 10) { Asset.Images.Scene.Social.connectHintBannerIcon.asImage() .resizable() .aspectRatio(contentMode: .fit) .frame(width: 24) - + Text(L10n.Plugins.Luckydrop.Confirm.tips) .font(FontStyles.rh6.font) .foregroundColor(Asset.Colors.Public.info.asColor()) @@ -29,23 +29,25 @@ struct UnlockNFTView: View { .background(Asset.Colors.Public.infoBg.asColor()) .cornerRadius(12) } - + var body: some View { VStack(spacing: 20) { Text(L10n.Scene.WalletUnlock.button + viewModel.tokenName) .font(FontStyles.bh4.font) .foregroundColor(Asset.Colors.Text.dark.asColor()) - - VStack(spacing: 8){ - + + VStack(spacing: 8) { if let ensName = viewModel.ensName { Text(ensName) .font(FontStyles.bh5.font) .foregroundColor(Asset.Colors.Text.dark.asColor()) HStack(spacing: 4) { Text(viewModel.address) + .truncationMode(.middle) .font(FontStyles.rh7.font) .foregroundColor(Asset.Colors.Text.dark.asColor()) + .frame(maxWidth: 118) + .fixedSize() Asset.Plugins.LuckyDrop.share.asImage() .frame(width: 20, height: 20) } @@ -59,14 +61,16 @@ struct UnlockNFTView: View { } } } - + VStack(spacing: 16) { buildRow( title: L10n.Plugins.Luckydrop.Confirm.walletAccount, value: .address(viewModel.address) ) - buildRow(title: L10n.Plugins.Luckydrop.Confirm.walletAccount, - value: .address(viewModel.contractAddress)) + buildRow( + title: L10n.Plugins.Luckydrop.Confirm.walletAccount, + value: .address(viewModel.contractAddress) + ) buildRow( title: L10n.Plugins.Luckydrop.transactionFee, value: .gas(viewModel.gasFeeInfo) @@ -75,7 +79,6 @@ struct UnlockNFTView: View { title: L10n.Plugins.Luckydrop.Confirm.totalAmount, value: .plain("\(viewModel.totalAmount)") ) - buildRow(title: L10n.Plugins.Luckydrop.Confirm.transactionFee, value: .gas(viewModel.gasFeeInfo)) tipsView } PrimaryButton( @@ -88,7 +91,7 @@ struct UnlockNFTView: View { } .padding(.horizontal) } - + @ViewBuilder func buildRow(title: String, value: ValueType) -> some View { HStack { @@ -96,14 +99,14 @@ struct UnlockNFTView: View { .font(FontStyles.rh6.font) .foregroundColor(Asset.Colors.Text.dark.asColor()) Spacer() - if case .plain(let title) = value { + if case let .plain(title) = value { Text(title) .truncationMode(.middle) .font(FontStyles.bh6.font) .foregroundColor(Asset.Colors.Text.dark.asColor()) .frame(maxWidth: 118) .fixedSize() - } else if case .address(let value) = value { + } else if case let .address(value) = value { Button { mainCoordinator.present(scene: .safariView(url: viewModel.operatorUrl), transition: .modal(animated: true)) } label: { @@ -118,7 +121,7 @@ struct UnlockNFTView: View { .frame(width: 20, height: 20) } } - } else if case .gas(let value) = value { + } else if case let .gas(value) = value { Button { mainCoordinator.present( scene: .gasFee( @@ -150,9 +153,8 @@ extension UnlockNFTView { } } - -//struct UnlockNFTView_Previews: PreviewProvider { +// struct UnlockNFTView_Previews: PreviewProvider { // static var previews: some View { // UnlockNFTView() // } -//} +// } diff --git a/Maskbook/Scene/App/RedPacket/ViewModel/NftLuckyDropViewModel.swift b/Maskbook/Scene/App/RedPacket/ViewModel/NftLuckyDropViewModel.swift index 1a11f9f7..b3d3ea83 100644 --- a/Maskbook/Scene/App/RedPacket/ViewModel/NftLuckyDropViewModel.swift +++ b/Maskbook/Scene/App/RedPacket/ViewModel/NftLuckyDropViewModel.swift @@ -139,7 +139,7 @@ extension NftLuckyDropViewModel { return false } - return WalletSendHelper.isApprovedForAll(contractAddress: contractAddress, fromAddress: walletAddress) + return false } private func checkState() -> TransactionState { @@ -222,7 +222,7 @@ extension NftLuckyDropViewModel { return } let name = groupName ?? "" - let identifiers = collectibles.map { $0.id ?? "" } + let identifiers = collectibles.map { $0.identifier ?? "" } action( .addCollectibles( groupName: name, @@ -281,14 +281,15 @@ extension NftLuckyDropViewModel { case .unlockWallet: action(.unlockWallet) case .confirmRisk: action(.confirmRisk) case .unlockNFT: - guard let contractAddress = collectibleGroup?.address, let item = gasFeeItem else { + guard let groupInfo = collectibleGroup, let item = gasFeeItem else { return } action( .unlockNFT( - contractAddress: contractAddress, - gasItem: item + collectibleGroup: groupInfo, + gasItem: item, + gasFeeViewModel: gasFeeViewModel ) ) @@ -315,7 +316,7 @@ extension NftLuckyDropViewModel { case createNFTLuckyDrop(draft: NftRecpacketDraft, gasFeeViewModel: GasFeeViewModel) case selectCollectibleGroup case unlockWallet - case unlockNFT(contractAddress: String, gasItem: GasFeeCellItem) + case unlockNFT(collectibleGroup: CollectiableGroup, gasItem: GasFeeCellItem, gasFeeViewModel: GasFeeViewModel) } enum CollectibleItem: Identifiable { @@ -326,8 +327,8 @@ extension NftLuckyDropViewModel { var id: String { switch self { case .add: return "add" - case let .normal(item): return item.id ?? "" - case let .all(item): return item.id ?? "" + case let .normal(item): return item.identifier ?? "" + case let .all(item): return item.identifier ?? "" } } } @@ -369,18 +370,26 @@ extension NftLuckyDropViewModel { } extension NftLuckyDropViewModel: ChooseCollectionBackDelegate { - func chooseNFTCollectionAction(token: Collectible) { + func chooseNFTCollectionAction(token: [Collectible]) { + + guard let asset = token.first else { + return + } + selectCollectible( - groupName: token.name ?? "", - contractAddress: token.address ?? "", - groupIconURL: URL(string: token.collectionImage ?? ""), - totalCount: 0 + groupName: asset.collectionName ?? "", + contractAddress: asset.address ?? "", + groupIconURL: URL(string: asset.collectionImage ?? ""), + totalCount: token.count ) + actionState = self.checkState() } } extension NftLuckyDropViewModel: SearchSingleNFTDelegate { func returnSelectedNFT(collectible: [Collectible]?) { addCollectibles(collectible ?? []) + actionState = self.checkState() + } } diff --git a/Maskbook/Scene/App/RedPacket/ViewModel/UnlockNFTViewModel.swift b/Maskbook/Scene/App/RedPacket/ViewModel/UnlockNFTViewModel.swift index 95554d41..bc058e1b 100644 --- a/Maskbook/Scene/App/RedPacket/ViewModel/UnlockNFTViewModel.swift +++ b/Maskbook/Scene/App/RedPacket/ViewModel/UnlockNFTViewModel.swift @@ -6,111 +6,112 @@ // Copyright © 2022 dimension. All rights reserved. // -import Foundation +import BigInt import Combine import CoreDataStack +import Foundation import web3swift -import BigInt class UnlockNFTViewModel: ObservableObject { @Published var gasFeeItem: GasFeeCellItem? - @Published var token: Collectible? + @Published var groupInfo: CollectiableGroup? @Published var buttonState: ConfirmButtonState = .normal var completion: ((String?, Error?) -> Void)? - var gasFeeViewModel: GasFeeViewModel? - var transaction: EthereumTransaction? - var options: TransactionOptions? @InjectedProvider(\.userDefaultSettings) var userSetting - + @InjectedProvider(\.walletAssetManager) private var walletAssetManager: WalletAssetManager - + + @InjectedProvider(\.vault) + var vault + + @InjectedProvider(\.userDefaultSettings) + private var settings + @InjectedProvider(\.walletConnectClient) + private var walletConnectClient + + private var disposeBag = Set() + var isSending: Bool { buttonState == .sending } - + var tokenName: String { - guard let name = token?.tokenName else { + guard let name = groupInfo?.name else { return "" } return name } - + var ensName: String? { - guard let address = userSetting.defaultAccountAddress else { return nil } - + guard let account = WalletCoreStorage.getAccount(address: address) else { return nil } - + guard let ensName = account.ensName else { return nil } - return ensName + return ensName } - + var address: String { - guard let address = userSetting.defaultAccountAddress else { return "" } - + return address } - - - + var contractAddress: String { - guard let contract = token?.address else { + guard let contract = groupInfo?.address else { return "" } - + return contract } - + var operatorUrl: URL { - guard let address = userSetting.network.nftRedPacketAddress else { return URL(string: "https://etherscan.io/address/0x8d285739523FC2Ac8eC9c9C229ee863C8C9bF8C8")! } - + guard let url = maskUserDefaults.network.getAddressUrl(address: address) else { return URL(string: "https://etherscan.io/address/0x8d285739523FC2Ac8eC9c9C229ee863C8C9bF8C8")! } return url } - + var gasLimit: BigUInt { - gasFeeViewModel?.gasLimitPublisher.value ?? BigUInt(10_000) + gasFeeViewModel?.gasLimitPublisher.value ?? BigUInt(10000) } - + var gasFeeInfo: String { guard let gasFeeItem = gasFeeItem else { return "" } - + guard let symbol = walletAssetManager.getDefaultMainToken()?.symbol else { return "" } - + let gasPrice = EthUtil.getGasFeeToken(gwei: gasFeeItem.gWei, gasLimit: gasLimit.description) - + return "\(gasPrice) \(symbol)" - } - + var totalAmount: String { guard let gasFeeItem = gasFeeItem else { return "" } - + guard let tokenPrice = walletAssetManager.getDefaultMainToken()?.price as? Double else { return "" } @@ -121,28 +122,167 @@ class UnlockNFTViewModel: ObservableObject { if gasPriceDoubleValue < 0.01 { gasPrice = "< \(symbol)0.01" } else { - gasPrice = "~\(symbol)\(EthUtil.getGasFeeFiat(gwei: gasFeeItem.gWei, gasLimit: gasLimit.description, price: tokenPrice))" + gasPrice = "~\(symbol)\(EthUtil.getGasFeeFiat(gwei: gasFeeItem.gWei, gasLimit: gasLimit.description, price: tokenPrice))" } - + return "\(gasPrice) \(symbol)" } - + + init( + gasFeeViewModel: GasFeeViewModel, + gasFeeItem: GasFeeCellItem, + collectibleGroup: CollectiableGroup, + completion: ((String?, Error?) -> Void)? + ) { + self.gasFeeViewModel = gasFeeViewModel + self.gasFeeItem = gasFeeItem + groupInfo = collectibleGroup + self.completion = completion + requestEstimateGasLimit() + } + @MainActor func onConfirm() { buttonState = .sending + unlockNFT() { [weak self] result in + DispatchQueue.main.async { + switch result { + case .success(let txHash): + self?.completion?(txHash, nil) + self?.completion = nil + + case .failure(let error): + log.error("send failed: \(error)", source: "lucky drop") + self?.completion?(nil, error) + self?.completion = nil + } + self?.buttonState = .normal + } + } + } + + @MainActor + private func unlockNFT(_ completion: @escaping (Swift.Result) -> Void) + { + guard let fromAddress = userSetting.defaultAccountAddress else { + completion(.failure(WalletSendError.addressError)) + return + } + guard let fromAccount = WalletCoreService.shared.getAccount(address: fromAddress) else { + completion(.failure(WalletSendError.addressError)) + return + } + + guard let contractAddress = groupInfo?.address else { + return + } + + guard let gasFeeItem = gasFeeItem else { + return + } + + guard let gasFeeViewModel = gasFeeViewModel else { + return + } + + guard let gasPrice = Web3.Utils.parseToBigUInt(gasFeeItem.gWei, units: .Gwei) else { + return + } + + guard let priorityFee = Web3.Utils.parseToBigUInt(gasFeeItem.suggestedMaxPriorityFeePerGas, units: .Gwei) else { + return + } + + guard let maxFee = Web3.Utils.parseToBigUInt(gasFeeItem.suggestedMaxFeePerGas, units: .Gwei) else { + return + } + + let completionWrapper: (Swift.Result) -> Void = { [weak self] result in + switch result { + case let .success(hash): + completion(.success(hash)) + + case let .failure(error): + DispatchQueue.main.async { + self?.buttonState = .normal + let alertController = AlertController( + title: "", + message: error.errorDescription, + confirmButtonText: L10n.Common.Controls.done, + imageType: .error, + confirmButtonClicked: { _ in + completion(.failure(error)) + } + ) + Coordinator.main.present( + scene: .alertController(alertController: alertController), + transition: .alertController(completion: nil) + ) + } + } + } + + let maxFeePerGas = Web3.Utils.parseToBigUInt(gasFeeItem.suggestedMaxFeePerGas, units: .Gwei) + let maxInclusionFeePerGas = Web3.Utils.parseToBigUInt( + gasFeeItem.suggestedMaxPriorityFeePerGas, + units: .Gwei + ) + + vault.getWalletPassword() + .sink(receiveCompletion: { _ in }) { [weak self] password in + // should run in main thread! + guard let self = self else { + return + } + + WalletSendHelper.setApproveAll(password: password, gasLimit: gasFeeViewModel.gasLimitPublisher.value, gasPrice: gasPrice, maxFeePerGas: maxFeePerGas == 0 ? nil : maxFeePerGas, maxInclusionFeePerGas: maxInclusionFeePerGas == 0 ? nil : maxInclusionFeePerGas, contractAddress: contractAddress, fromAddress: fromAddress, network: self.settings.network, completionWrapper) + } + .store(in: &disposeBag) } - - + private func requestEstimateGasLimit() { - guard let web3Provier = Web3ProviderFactory.provider?.eth else { return } - guard let transaction = transaction else { return } - _ = web3Provier.estimateGasPromise(transaction, transactionOptions: options) - .done { [weak self] gaslimit in - self?.gasFeeViewModel?.gasLimitPublisher.accept(gaslimit) + guard let provider = Web3ProviderFactory.provider else { + return + } + + guard let web3Provier = Web3ProviderFactory.provider?.eth else { + return + } + + guard let contractAddressEthFormat = EthereumAddress(contractAddress) else { + return + } + + guard let fromAddressEthFormat = EthereumAddress(address) else { + return + } + + DispatchQueue.global().async { + let erc721 = ERC721(web3: provider, provider: provider.provider, address: contractAddressEthFormat) + + let preTransacation = try? erc721.setApprovalForAll(from: fromAddressEthFormat, operator: contractAddressEthFormat, approved: true) + + guard let preTransacation = preTransacation else { + return } - .catch { _ in + + let transacationResult = try? preTransacation.assemble() + + guard let transacationResult = transacationResult else { + return } + + _ = web3Provier.estimateGasPromise(transacationResult, transactionOptions: TransactionOptions.defaultOptions) + .done { [weak self] gaslimit in + + DispatchQueue.main.async { + self?.gasFeeViewModel?.gasLimitPublisher.accept(gaslimit) + } + } + .catch { _ in + } + } } } @@ -151,5 +291,4 @@ extension UnlockNFTViewModel { case normal case sending } - } diff --git a/Maskbook/Scene/Wallet/WalletSend/SearchNFTCollectionViewController.swift b/Maskbook/Scene/Wallet/WalletSend/SearchNFTCollectionViewController.swift index 0262d26d..99168fa4 100644 --- a/Maskbook/Scene/Wallet/WalletSend/SearchNFTCollectionViewController.swift +++ b/Maskbook/Scene/Wallet/WalletSend/SearchNFTCollectionViewController.swift @@ -13,7 +13,7 @@ import PanModal import UIKit protocol ChooseCollectionBackDelegate: AnyObject { - func chooseNFTCollectionAction(token: Collectible) + func chooseNFTCollectionAction(token: [Collectible]) } class SearchNFTCollectionViewController: BaseViewController { @@ -131,10 +131,7 @@ extension SearchNFTCollectionViewController: UITableViewDelegate { guard let delegate = delegate else { return } - guard let collection = assets.first else { - return - } - delegate.chooseNFTCollectionAction(token: collection) + delegate.chooseNFTCollectionAction(token: assets) navigationController?.popViewController(animated: true) } } diff --git a/Maskbook/Scene/Wallet/WalletSend/SearchSingleNFTViewController.swift b/Maskbook/Scene/Wallet/WalletSend/SearchSingleNFTViewController.swift index 3bc89647..949e5c7e 100644 --- a/Maskbook/Scene/Wallet/WalletSend/SearchSingleNFTViewController.swift +++ b/Maskbook/Scene/Wallet/WalletSend/SearchSingleNFTViewController.swift @@ -24,8 +24,8 @@ class SearchSingleNFTViewController: BaseViewController { var isSelectedAll: Bool = false - init(contractAddress: String) { - viewModel = SearchSingleNFTViewModel(contractAddress: contractAddress) + init(contractAddress: String, selectedIdentifiers: Set) { + viewModel = SearchSingleNFTViewModel(contractAddress: contractAddress, selectedIdentifiers:selectedIdentifiers) super.init(nibName: nil, bundle: nil) } diff --git a/Maskbook/Scene/Wallet/WalletSend/ViewModel/SearchSingleNFTViewModel.swift b/Maskbook/Scene/Wallet/WalletSend/ViewModel/SearchSingleNFTViewModel.swift index 9833fe8b..264fad7f 100644 --- a/Maskbook/Scene/Wallet/WalletSend/ViewModel/SearchSingleNFTViewModel.swift +++ b/Maskbook/Scene/Wallet/WalletSend/ViewModel/SearchSingleNFTViewModel.swift @@ -44,13 +44,19 @@ class SearchSingleNFTViewModel { var selectedCollectibles = CurrentValueSubject<[Collectible], Never>([]) var searchString = CurrentValueSubject("") - init(contractAddress: String) { + init(contractAddress: String, selectedIdentifiers: Set) { collectiblesPublisher = SearchSingleNFTViewModel.collectiblesPublisher(contractAdress: contractAddress) collectiblesPublisher? .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] collectibles in self?.collectiblesSubject.value = collectibles + self?.selectedCollectibles.value = collectibles.filter({ collectibles in + guard let identifier = collectibles.identifier else { + return false + } + return selectedIdentifiers.contains(identifier) + }) }) .store(in: &disposeBag) diff --git a/Maskbook/Supporting Files/Coordinator.swift b/Maskbook/Supporting Files/Coordinator.swift index c65c268b..e224d712 100644 --- a/Maskbook/Supporting Files/Coordinator.swift +++ b/Maskbook/Supporting Files/Coordinator.swift @@ -214,6 +214,12 @@ class Coordinator { options: TransactionOptions, completion: (String?, Error?) -> Void ) + case unlockNFT( + gasFeeViewModel: GasFeeViewModel, + gasFeeItem: GasFeeCellItem, + groupInfo: CollectiableGroup, + completion: (String?, Error?) -> Void + ) case luckyDropSuccessfully(callback: (@MainActor () -> Void)?) case luckyDropCreatePersona(callback: (@MainActor () -> Void)?) case luckyDropCreateProfile @@ -225,7 +231,7 @@ class Coordinator { case fileServiceDetail(FileServiceDownloadItem) case fileServiceFAQ case luckydropSearchCollection(delegate: ChooseCollectionBackDelegate?) - case luckydropSearchNFT(delegate: SearchSingleNFTDelegate?, contractAddress: String) + case luckydropSearchNFT(delegate: SearchSingleNFTDelegate?, contractAddress: String, selectedIdentifiers: Set) case messageCompose(PluginMeta? = nil) case composeSelectContact(viewModel: SelectContactViewModel) case debug @@ -855,6 +861,18 @@ extension Coordinator { options: options, completion: completion ) + case .unlockNFT( + let gasFeeViewModel, + let gasFeeItem, + let groupInfo, + let completion + ): + return UnlockNFTViewController( + gasFeeViewModel: gasFeeViewModel, + gasFeeItem: gasFeeItem, + groupInfo: groupInfo, + completion: completion + ) case let .luckyDropSuccessfully(callback): let viewModel = LuckyDropSuccessfullyViewModel(callback: callback) @@ -892,8 +910,8 @@ extension Coordinator { searchCollectionVc.delegate = selectionDelegate return searchCollectionVc - case let .luckydropSearchNFT(selectionDelegate, contractAddress): - let searchNFTVc = SearchSingleNFTViewController(contractAddress: contractAddress) + case let .luckydropSearchNFT(selectionDelegate, contractAddress, selectedIdentifiers): + let searchNFTVc = SearchSingleNFTViewController(contractAddress: contractAddress, selectedIdentifiers:selectedIdentifiers) searchNFTVc.delegate = selectionDelegate return searchNFTVc diff --git a/Maskbook/Wallet/WalletCore/WalletSendHelper.swift b/Maskbook/Wallet/WalletCore/WalletSendHelper.swift index 769b2c1e..5242c66c 100644 --- a/Maskbook/Wallet/WalletCore/WalletSendHelper.swift +++ b/Maskbook/Wallet/WalletCore/WalletSendHelper.swift @@ -387,28 +387,37 @@ class WalletSendHelper { class func isApprovedForAll( contractAddress: String, - fromAddress: String - ) -> Bool { + fromAddress: String, + _ completion: @escaping (Bool) -> Void + ) + { guard let provider = Web3ProviderFactory.provider else { - return false + completion(false) + return } guard let contractAddressEthFormat = EthereumAddress(contractAddress) else { - return false + completion(false) + return } guard let fromAddressEthFormat = EthereumAddress(fromAddress) else { - return false + completion(false) + return } + DispatchQueue.global().async { + let erc721 = ERC721(web3: provider, provider: provider.provider, address: contractAddressEthFormat) - let erc721 = ERC721(web3: provider, provider: provider.provider, address: contractAddressEthFormat) - - let isApproved = try? erc721.isApprovedForAll(owner: fromAddressEthFormat, operator: contractAddressEthFormat) + let isApproved = try? erc721.isApprovedForAll(owner: fromAddressEthFormat, operator: contractAddressEthFormat) - guard let isApproved = isApproved else { - return false + guard let isApproved = isApproved else { + completion(false) + return + } + DispatchQueue.main.async { + completion(isApproved) + } } - return isApproved } class func setApproveAll( @@ -420,7 +429,7 @@ class WalletSendHelper { contractAddress: String, fromAddress: String, network: BlockChainNetwork, - _ completion: @escaping (Result) -> Void + _ completion: @escaping (Result) -> Void ) { guard let provider = Web3ProviderFactory.provider else { completion(.failure(WalletSendError.unsupportedChainType)) @@ -442,58 +451,67 @@ class WalletSendHelper { completion(.failure(WalletSendError.addressError)) return } + + DispatchQueue.global().async { + let erc721 = ERC721(web3: provider, provider: provider.provider, address: contractAddressEthFormat) - let erc721 = ERC721(web3: provider, provider: provider.provider, address: contractAddressEthFormat) - - let preTransacation = try? erc721.setApprovalForAll(from: fromAddressEthFormat, operator: contractAddressEthFormat, approved: true) + let preTransacation = try? erc721.setApprovalForAll(from: fromAddressEthFormat, operator: contractAddressEthFormat, approved: true) - guard let transacation = preTransacation else { - completion(.failure(WalletSendError.passwordError)) - return - } + guard let transacation = preTransacation else { + completion(.failure(WalletSendError.passwordError)) + return + } - do { - let transacationResult = try transacation.assemble() do { - let nonce = try provider.eth.getTransactionCount(address: fromAddressEthFormat) - var gasPriceHex = "0x0" - var maxInclusionFeePerGasHex = "0x0" - var maxFeePerGasHex = "0x0" - if maxFeePerGas == nil || maxInclusionFeePerGas == nil { - gasPriceHex = gasPrice!.serialize().toHexString().addHexPrefix() - } else { - maxInclusionFeePerGasHex = maxInclusionFeePerGas!.serialize().toHexString().addHexPrefix() - maxFeePerGasHex = maxFeePerGas!.serialize().toHexString().addHexPrefix() - } - let signInput = - WalletCoreHelper.SignInput.eth( - chainId: Int64(network.networkId), - nonce: nonce.serialize().toHexString().addHexPrefix(), - gasPrice: gasPriceHex, - maxInclusionFeePerGas: maxInclusionFeePerGasHex, - maxFeePerGas: maxFeePerGasHex, - gasLimit: gasLimit.serialize().toHexString().addHexPrefix(), - amount: "0x0", - toAddress: contractAddress, - payload: transacationResult.data - ) - let result = - WalletCoreHelper.signTransaction(password: password, storedKeyData: storedKeyData, derivationPath: derivationPath, input: signInput, chainType: network.chain) - switch result { - case let .success(signOutput): - switch signOutput { - case let .eth(transacationEncodeData, _, _, _, _): - do { - let sendResult = try provider.eth.sendRawTransaction(transacationEncodeData) - completion(.success(sendResult.transaction)) - return - } catch { - completion(.failure(WalletSendError.ethereumError(error))) - return - } + let transacationResult = try transacation.assemble() + do { + let nonce = try provider.eth.getTransactionCount(address: fromAddressEthFormat) + var gasPriceHex = "0x0" + var maxInclusionFeePerGasHex = "0x0" + var maxFeePerGasHex = "0x0" + if maxFeePerGas == nil || maxInclusionFeePerGas == nil { + gasPriceHex = gasPrice!.serialize().toHexString().addHexPrefix() + } else { + maxInclusionFeePerGasHex = maxInclusionFeePerGas!.serialize().toHexString().addHexPrefix() + maxFeePerGasHex = maxFeePerGas!.serialize().toHexString().addHexPrefix() } + let signInput = + WalletCoreHelper.SignInput.eth( + chainId: Int64(network.networkId), + nonce: nonce.serialize().toHexString().addHexPrefix(), + gasPrice: gasPriceHex, + maxInclusionFeePerGas: maxInclusionFeePerGasHex, + maxFeePerGas: maxFeePerGasHex, + gasLimit: gasLimit.serialize().toHexString().addHexPrefix(), + amount: "0x0", + toAddress: contractAddress, + payload: transacationResult.data + ) + let result = + WalletCoreHelper.signTransaction(password: password, storedKeyData: storedKeyData, derivationPath: derivationPath, input: signInput, chainType: network.chain) + switch result { + case let .success(signOutput): + switch signOutput { + case let .eth(transacationEncodeData, _, _, _, _): + do { + let sendResult = try provider.eth.sendRawTransaction(transacationEncodeData) + DispatchQueue.main.async { + completion(.success(sendResult.hash)) + } + return + } catch { + DispatchQueue.main.async { + completion(.failure(WalletSendError.ethereumError(error))) + } + return + } + } - case let .failure(error): + case let .failure(error): + completion(.failure(WalletSendError.ethereumError(error))) + return + } + } catch { completion(.failure(WalletSendError.ethereumError(error))) return } @@ -501,9 +519,6 @@ class WalletSendHelper { completion(.failure(WalletSendError.ethereumError(error))) return } - } catch { - completion(.failure(WalletSendError.ethereumError(error))) - return } } }