diff --git a/Kukai Mobile.xcodeproj/project.pbxproj b/Kukai Mobile.xcodeproj/project.pbxproj index 6c110bde..5fc702b5 100644 --- a/Kukai Mobile.xcodeproj/project.pbxproj +++ b/Kukai Mobile.xcodeproj/project.pbxproj @@ -192,6 +192,7 @@ C06EA1A626A56C6E006029CF /* OnboardingPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06EA1A526A56C6E006029CF /* OnboardingPageViewController.swift */; }; C06F28222A0E811D00543E31 /* AccountsContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06F28212A0E811D00543E31 /* AccountsContainerViewController.swift */; }; C0717B1E2A697D3D007F9419 /* EnterCustomBakerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0717B1D2A697D3D007F9419 /* EnterCustomBakerViewController.swift */; }; + C073938D2B50053500344DBC /* SendGenericConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C073938C2B50053500344DBC /* SendGenericConfirmViewController.swift */; }; C073C0DE29E6CADC0064FBEF /* LoadingContainerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C073C0DD29E6CADC0064FBEF /* LoadingContainerCell.swift */; }; C0742C53297ADC8E005D6DB0 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0742C52297ADC8E005D6DB0 /* MenuViewController.swift */; }; C074EC632AF50CBA008B2F56 /* Test_10_ConnectedApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074EC622AF50CBA008B2F56 /* Test_10_ConnectedApps.swift */; }; @@ -596,6 +597,7 @@ C06EA1A526A56C6E006029CF /* OnboardingPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageViewController.swift; sourceTree = ""; }; C06F28212A0E811D00543E31 /* AccountsContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsContainerViewController.swift; sourceTree = ""; }; C0717B1D2A697D3D007F9419 /* EnterCustomBakerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterCustomBakerViewController.swift; sourceTree = ""; }; + C073938C2B50053500344DBC /* SendGenericConfirmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendGenericConfirmViewController.swift; sourceTree = ""; }; C073C0DD29E6CADC0064FBEF /* LoadingContainerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContainerCell.swift; sourceTree = ""; }; C0742C52297ADC8E005D6DB0 /* MenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; C074EC622AF50CBA008B2F56 /* Test_10_ConnectedApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test_10_ConnectedApps.swift; sourceTree = ""; }; @@ -1408,6 +1410,7 @@ C0A9865629895A6A0092EA2F /* SendCollectibleAmountViewController.swift */, C0FAD1D52992992500FB813B /* SendCollectibleConfirmViewController.swift */, C0151B262A15256900C7F250 /* SendContractConfirmViewController.swift */, + C073938C2B50053500344DBC /* SendGenericConfirmViewController.swift */, ); path = Send; sourceTree = ""; @@ -1961,6 +1964,7 @@ C0FA74DC2A4B011200CA845B /* LoadingCollectibleCell.swift in Sources */, C08694EC27BD5F8C000A4909 /* RequestIfService.swift in Sources */, C07538152AAB65CA0041550F /* SideMenuAboutCell.swift in Sources */, + C073938D2B50053500344DBC /* SendGenericConfirmViewController.swift in Sources */, C083E65B2A6EC7D600B3BEBE /* DiscoverHeadingCell.swift in Sources */, C049E73A2881837F00887B64 /* AccountsViewController.swift in Sources */, C044E1372AAF66F80085652F /* SideMenuSecurityViewModel.swift in Sources */, diff --git a/Kukai Mobile/Modules/Home/Base.lproj/Home.storyboard b/Kukai Mobile/Modules/Home/Base.lproj/Home.storyboard index 1e1f8ca7..b4b70f79 100644 --- a/Kukai Mobile/Modules/Home/Base.lproj/Home.storyboard +++ b/Kukai Mobile/Modules/Home/Base.lproj/Home.storyboard @@ -1,9 +1,9 @@ - + - + @@ -113,6 +113,7 @@ + @@ -197,13 +198,13 @@ - + @@ -746,7 +747,7 @@ - + @@ -1365,10 +1366,18 @@ + + + + + + + + - + @@ -1380,7 +1389,7 @@ - + @@ -1398,13 +1407,13 @@ - + - + - + diff --git a/Kukai Mobile/Modules/Home/HomeTabBarController.swift b/Kukai Mobile/Modules/Home/HomeTabBarController.swift index 5553b4dd..c6616756 100644 --- a/Kukai Mobile/Modules/Home/HomeTabBarController.swift +++ b/Kukai Mobile/Modules/Home/HomeTabBarController.swift @@ -493,6 +493,9 @@ extension HomeTabBarController: WalletConnectServiceDelegate { case .contractCall: self.performSegue(withIdentifier: "wallet-connect-contract", sender: nil) + + case .generic: + self.performSegue(withIdentifier: "wallet-connect-generic", sender: nil) } } } diff --git a/Kukai Mobile/Modules/Send/Base.lproj/Send.storyboard b/Kukai Mobile/Modules/Send/Base.lproj/Send.storyboard index 2b68e572..d69f2db6 100644 --- a/Kukai Mobile/Modules/Send/Base.lproj/Send.storyboard +++ b/Kukai Mobile/Modules/Send/Base.lproj/Send.storyboard @@ -1,9 +1,9 @@ - + - + @@ -372,26 +372,26 @@ - + - + @@ -527,22 +527,22 @@ - + - + @@ -603,29 +603,29 @@ - + - + - + - + @@ -845,32 +845,32 @@ - + - + - + - + - + @@ -904,7 +904,7 @@ - + @@ -914,29 +914,29 @@ - + - + - + @@ -977,13 +977,13 @@ - + @@ -1019,19 +1019,19 @@ - + @@ -1308,11 +1308,11 @@ - + - + @@ -2057,32 +2057,32 @@ - + - + - + - + - + @@ -2116,7 +2116,7 @@ - + @@ -2126,29 +2126,29 @@ - + - + - + @@ -2180,7 +2180,7 @@ - + @@ -2202,7 +2202,7 @@ - + @@ -2221,7 +2221,7 @@ - + - + - + - + - + @@ -2432,7 +2432,7 @@ - + @@ -2473,11 +2473,11 @@ - + - + @@ -2810,32 +2810,32 @@ - + - + - + - + - + @@ -2869,7 +2869,7 @@ - + @@ -2879,29 +2879,29 @@ - + - + - + @@ -2942,13 +2942,13 @@ - + @@ -2984,19 +2984,19 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + + + + - + + + + - + - + - + + + + - + - + - + - + - + + + + - + @@ -3567,6 +4130,7 @@ + diff --git a/Kukai Mobile/Modules/Send/SendGenericConfirmViewController.swift b/Kukai Mobile/Modules/Send/SendGenericConfirmViewController.swift new file mode 100644 index 00000000..364682d7 --- /dev/null +++ b/Kukai Mobile/Modules/Send/SendGenericConfirmViewController.swift @@ -0,0 +1,264 @@ +// +// SendGenericConfirmViewController.swift +// Kukai Mobile +// +// Created by Simon Mcloughlin on 11/01/2024. +// + +import UIKit +import KukaiCoreSwift +import WalletConnectSign +import OSLog + +class SendGenericConfirmViewController: SendAbstractConfirmViewController, SlideButtonDelegate, EditFeesViewControllerDelegate { + + @IBOutlet var scrollView: UIScrollView! + + // Connected app + @IBOutlet weak var connectedAppLabel: UILabel! + @IBOutlet weak var connectedAppIcon: UIImageView! + @IBOutlet weak var connectedAppNameLabel: UILabel! + @IBOutlet weak var connectedAppMetadataStackView: UIStackView! + + // From + @IBOutlet weak var fromContainer: UIView! + + @IBOutlet weak var fromStackViewSocial: UIStackView! + @IBOutlet weak var fromSocialIcon: UIImageView! + @IBOutlet weak var fromSocialAlias: UILabel! + @IBOutlet weak var fromSocialAddress: UILabel! + + @IBOutlet weak var fromStackViewRegular: UIStackView! + @IBOutlet weak var fromRegularAddress: UILabel! + + // Operation + @IBOutlet weak var moreButton: CustomisableButton! + @IBOutlet weak var operationTextView: UITextView! + + // Fee + @IBOutlet weak var feeValueLabel: UILabel! + @IBOutlet weak var feeButton: CustomisableButton! + @IBOutlet weak var slideErrorStackView: UIStackView! + @IBOutlet weak var ledgerWarningLabel: UILabel! + @IBOutlet weak var errorLabel: UILabel! + @IBOutlet weak var slideButton: SlideButton! + @IBOutlet weak var testnetWarningView: UIView! + + var dimBackground: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + let _ = self.view.addGradientBackgroundFull() + + feeButton.accessibilityIdentifier = "fee-button" + + if DependencyManager.shared.currentNetworkType != .testnet { + testnetWarningView.isHidden = true + } + + + // Handle wallet connect data + if let currentTopic = TransactionService.shared.walletConnectOperationData.request?.topic, + let session = Sign.instance.getSessions().first(where: { $0.topic == currentTopic }) { + + guard let account = WalletConnectService.accountFromRequest(TransactionService.shared.walletConnectOperationData.request), + let walletMetadataForRequestedAccount = DependencyManager.shared.walletList.metadata(forAddress: account) else { + self.windowError(withTitle: "error".localized(), description: "error-no-account".localized()) + self.handleRejection() + return + } + + self.isWalletConnectOp = true + self.currentContractData = TransactionService.shared.walletConnectOperationData.contractCallData + self.selectedMetadata = walletMetadataForRequestedAccount + self.connectedAppNameLabel.text = session.peer.name + + if let iconString = session.peer.icons.first, let iconUrl = URL(string: iconString) { + let smallIconURL = MediaProxyService.url(fromUri: iconUrl, ofFormat: .icon) + connectedAppURL = smallIconURL + } + + let media = TransactionService.walletMedia(forWalletMetadata: walletMetadataForRequestedAccount, ofSize: .size_22) + if let subtitle = media.subtitle { + fromStackViewRegular.isHidden = true + fromSocialAlias.text = media.title + fromSocialIcon.image = media.image + fromSocialAddress.text = subtitle + } else { + fromStackViewSocial.isHidden = true + fromRegularAddress.text = media.title + } + + } else { + self.isWalletConnectOp = false + self.currentContractData = TransactionService.shared.contractCallData + self.selectedMetadata = DependencyManager.shared.selectedWalletMetadata + + connectedAppMetadataStackView.isHidden = true + connectedAppLabel.isHidden = true + fromContainer.isHidden = true + } + + + // Display JSON + updateOperationDisplay() + + + // Fees + feeValueLabel.accessibilityIdentifier = "fee-amount" + feeButton.customButtonType = .secondary + updateFees() + + + // Ledger check + if selectedMetadata?.type != .ledger { + ledgerWarningLabel.isHidden = true + } + + + // Error / warning check (TBD) + errorLabel.isHidden = true + + + if ledgerWarningLabel.isHidden && errorLabel.isHidden { + slideErrorStackView.isHidden = true + } + + slideButton.delegate = self + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let connectedAppURL = connectedAppURL { + MediaProxyService.load(url: connectedAppURL, to: self.connectedAppIcon, withCacheType: .temporary, fallback: UIImage.unknownToken()) + } else { + self.connectedAppIcon.image = UIImage.unknownToken() + } + } + + private func selectedOperationsAndFees() -> [KukaiCoreSwift.Operation] { + if isWalletConnectOp { + return TransactionService.shared.currentRemoteOperationsAndFeesData.selectedOperationsAndFees() + + } else { + return TransactionService.shared.currentOperationsAndFeesData.selectedOperationsAndFees() + } + } + + func didCompleteSlide() { + self.showLoadingModal(invisible: true) { [weak self] in + self?.performAuth() + } + } + + override func authSuccessful() { + guard let walletAddress = selectedMetadata?.address, let wallet = WalletCacheService().fetchWallet(forAddress: walletAddress) else { + self.hideLoadingModal { [weak self] in + self?.windowError(withTitle: "error".localized(), description: "error-no-wallet-short".localized()) + self?.slideButton.resetSlider() + } + + return + } + + DependencyManager.shared.tezosNodeClient.send(operations: selectedOperationsAndFees(), withWallet: wallet) { [weak self] sendResult in + self?.slideButton.markComplete(withText: "Complete") + + self?.hideLoadingModal(invisible: true, completion: { [weak self] in + switch sendResult { + case .success(let opHash): + Logger.app.info("Sent: \(opHash)") + + self?.didSend = true + self?.addPendingTransaction(opHash: opHash) + self?.handleApproval(opHash: opHash) + + case .failure(let sendError): + self?.windowError(withTitle: "error".localized(), description: sendError.description) + self?.slideButton?.resetSlider() + } + }) + } + } + + override func authFailure() { + self.hideLoadingModal { [weak self] in + self?.slideButton.resetSlider() + } + } + + func updateOperationDisplay() { + let ops = selectedOperationsAndFees() + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + let data = (try? encoder.encode(ops)) ?? Data() + let string = String(data: data, encoding: .utf8) + operationTextView.text = string + } + + func updateFees() { + let feesAndData = isWalletConnectOp ? TransactionService.shared.currentRemoteOperationsAndFeesData : TransactionService.shared.currentOperationsAndFeesData + let fee = (feesAndData.fee + feesAndData.maxStorageCost) + + feeValueLabel.text = fee.normalisedRepresentation + " tez" + feeButton.setTitle(feesAndData.type.displayName(), for: .normal) + } + + @IBAction func closeTapped(_ sender: Any) { + handleRejection() + } + + @IBAction func copyTapped(_ sender: UIButton) { + Toast.shared.show(withMessage: "copied!", attachedTo: sender) + UIPasteboard.general.string = operationTextView.text + } + + func addPendingTransaction(opHash: String) { + guard let selectedWalletMetadata = selectedMetadata else { return } + + let destinationAddress = currentContractData.contractAddress ?? "" + let amount = currentContractData.chosenAmount ?? .zero() + + let currentOps = selectedOperationsAndFees() + let counter = Decimal(string: currentOps.last?.counter ?? "0") ?? 0 + let contractOp = OperationFactory.Extractor.firstContractCallOperation(operations: currentOps) + + let entrypoint = (contractOp?.parameters?["entrypoint"] as? String) ?? "" + let parameterValueDict = contractOp?.parameters?["value"] as? [String: String] ?? [:] + let parameterValueString = String(data: (try? JSONEncoder().encode(parameterValueDict)) ?? Data(), encoding: .utf8) + let parameters: [String: String] = ["entrypoint": entrypoint, "value": parameterValueString ?? ""] + + let addPendingResult = DependencyManager.shared.activityService.addPending(opHash: opHash, + type: .transaction, + counter: counter, + fromWallet: selectedWalletMetadata, + destinationAddress: destinationAddress, + destinationAlias: nil, + xtzAmount: amount, + parameters: parameters, + primaryToken: nil) + + DependencyManager.shared.activityService.addUniqueAddressToPendingOperation(address: selectedWalletMetadata.address) + Logger.app.info("Recorded pending transaction: \(addPendingResult)") + } +} + +extension SendGenericConfirmViewController: BottomSheetCustomCalculateProtocol { + + func bottomSheetHeight() -> CGFloat { + viewDidLoad() + + scrollView.setNeedsLayout() + view.setNeedsLayout() + scrollView.layoutIfNeeded() + view.layoutIfNeeded() + + var height = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + height += scrollView.contentSize.height + + return height + } +} diff --git a/Kukai Mobile/Services/WalletConnectService.swift b/Kukai Mobile/Services/WalletConnectService.swift index b65315b0..e0085cde 100644 --- a/Kukai Mobile/Services/WalletConnectService.swift +++ b/Kukai Mobile/Services/WalletConnectService.swift @@ -18,6 +18,7 @@ public enum WalletConnectOperationType { case sendToken case sendNft case contractCall + case generic } public protocol WalletConnectServiceDelegate: AnyObject { @@ -681,13 +682,14 @@ public class WalletConnectService { TransactionService.shared.currentRemoteOperationsAndFeesData = operationsObj TransactionService.shared.currentRemoteForgedString = estimationResult.forgedString + if let contractDetails = OperationFactory.Extractor.isContractCall(operations: operations) { let totalXTZ = OperationFactory.Extractor.totalXTZAmountForContractCall(operations: operations) TransactionService.shared.walletConnectOperationData.currentTransactionType = .contractCall TransactionService.shared.walletConnectOperationData.contractCallData = TransactionService.ContractCallData(chosenToken: Token.xtz(), chosenAmount: totalXTZ, contractAddress: contractDetails.address, operationCount: operations.count, mainEntrypoint: contractDetails.entrypoint) mainThreadProcessedOperations(ofType: .contractCall) - }/* else if let transactionOperation = OperationFactory.Extractor.isTezTransfer(operations: operations) as? OperationTransaction { + } else if let transactionOperation = OperationFactory.Extractor.isTezTransfer(operations: operations) as? OperationTransaction { DependencyManager.shared.tezosNodeClient.getBalance(forAddress: forWallet.address) { [weak self] res in let xtzAmount = XTZAmount(fromRpcAmount: transactionOperation.amount) ?? .zero() @@ -701,7 +703,7 @@ public class WalletConnectService { self?.mainThreadProcessedOperations(ofType: .sendToken) } - }*/ else if let result = OperationFactory.Extractor.faTokenDetailsFrom(operations: operationsObj.selectedOperationsAndFees()), + } else if let result = OperationFactory.Extractor.faTokenDetailsFrom(operations: operationsObj.selectedOperationsAndFees()), let token = DependencyManager.shared.balanceService.token(forAddress: result.tokenContract, andTokenId: result.tokenId) { TransactionService.shared.walletConnectOperationData.currentTransactionType = .send @@ -718,6 +720,7 @@ public class WalletConnectService { } } else { + /* var firstTransaction: OperationTransaction? = nil let totalAmount = operations.compactMap({ ($0 as? OperationTransaction)?.amount }).compactMap({ XTZAmount(fromRpcAmount: $0) }).reduce(XTZAmount.zero(), +) @@ -738,7 +741,9 @@ public class WalletConnectService { TransactionService.shared.walletConnectOperationData.sendData.chosenAmount = totalAmount TransactionService.shared.walletConnectOperationData.sendData.destination = firstTransaction?.destination ?? "" self?.mainThreadProcessedOperations(ofType: .sendToken) - } + }*/ + + self.mainThreadProcessedOperations(ofType: .generic) } }