From fbcfe5baccb89952b0f8a71232c1b61fcbc218e1 Mon Sep 17 00:00:00 2001 From: gemdev111 Date: Mon, 23 Sep 2024 19:20:39 +0300 Subject: [PATCH] Added WalletConnectManager responsible for handling wc-requests and transform existed requests to pending, if device in locked state --- Gem.xcodeproj/project.pbxproj | 6 + .../WalletCoordinator+WalletConnector.swift | 57 ++---- Gem/Core/Coordinator/WalletCoordinator.swift | 70 ++++--- Gem/Core/Services/WalletConnectManager.swift | 176 ++++++++++++++++++ 4 files changed, 234 insertions(+), 75 deletions(-) create mode 100644 Gem/Core/Services/WalletConnectManager.swift diff --git a/Gem.xcodeproj/project.pbxproj b/Gem.xcodeproj/project.pbxproj index 622e60e4..8fdc6a8d 100644 --- a/Gem.xcodeproj/project.pbxproj +++ b/Gem.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 833A0C852C80C11B0004DBA9 /* ChainsFilterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833A0C842C80C11B0004DBA9 /* ChainsFilterType.swift */; }; 833A0C872C80C2A50004DBA9 /* ChainsFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833A0C862C80C2A50004DBA9 /* ChainsFilterViewModel.swift */; }; 835644FD2C501C2200C6B4DB /* SwapError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835644FC2C501C2200C6B4DB /* SwapError.swift */; }; + 836CCBD92CA19B69002456A5 /* WalletConnectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836CCBD82CA19B69002456A5 /* WalletConnectManager.swift */; }; + 836CCBDA2CA19B69002456A5 /* WalletConnectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836CCBD82CA19B69002456A5 /* WalletConnectManager.swift */; }; 836EC8582C32B22F00970BF1 /* LatencyMeasureService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836EC8572C32B22F00970BF1 /* LatencyMeasureService.swift */; }; 8374DC5A2C6CDF6C00183109 /* AddTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8374DC592C6CDF6C00183109 /* AddTokenInput.swift */; }; 838F14382C3D476C0049A8D2 /* AddNodeResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F14372C3D476C0049A8D2 /* AddNodeResultViewModel.swift */; }; @@ -605,6 +607,7 @@ 833A0C842C80C11B0004DBA9 /* ChainsFilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainsFilterType.swift; sourceTree = ""; }; 833A0C862C80C2A50004DBA9 /* ChainsFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainsFilterViewModel.swift; sourceTree = ""; }; 835644FC2C501C2200C6B4DB /* SwapError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapError.swift; sourceTree = ""; }; + 836CCBD82CA19B69002456A5 /* WalletConnectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectManager.swift; sourceTree = ""; }; 836EC8572C32B22F00970BF1 /* LatencyMeasureService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatencyMeasureService.swift; sourceTree = ""; }; 8374DC592C6CDF6C00183109 /* AddTokenInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTokenInput.swift; sourceTree = ""; }; 838F14372C3D476C0049A8D2 /* AddNodeResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNodeResultViewModel.swift; sourceTree = ""; }; @@ -2145,6 +2148,7 @@ isa = PBXGroup; children = ( 830FDA442C224316003DA605 /* NavigationStateManager.swift */, + 836CCBD82CA19B69002456A5 /* WalletConnectManager.swift */, C3E99BC82A76BF8B005DF35F /* CleanUpService.swift */, C36679192A17507500F1D74D /* OnstartService.swift */, C3D1C4D42A24171B006E8EEA /* OnstartAsyncService.swift */, @@ -2600,6 +2604,7 @@ C3D1C5052A43AB04006E8EEA /* TransactionsScene.swift in Sources */, D820CB522B51ED220063D70C /* ValidatorImageView.swift in Sources */, 832A705E2C88CE0A00045367 /* SelectWalletScene.swift in Sources */, + 836CCBD92CA19B69002456A5 /* WalletConnectManager.swift in Sources */, D8C4A3852C9388C7006FABE8 /* PriceAlertsScene.swift in Sources */, D8385CEA2B017F4500F784C7 /* SwapViewModel.swift in Sources */, 830ED2362C63AE4D00B9877C /* FeeUnitViewModel.swift in Sources */, @@ -3021,6 +3026,7 @@ D8C4A3352C8A7B14006FABE8 /* CopyType.swift in Sources */, D8C4A3362C8A7B14006FABE8 /* AssetSceneViewModel.swift in Sources */, D8C4A3372C8A7B14006FABE8 /* PriceViewModel.swift in Sources */, + 836CCBDA2CA19B69002456A5 /* WalletConnectManager.swift in Sources */, D8C4A3382C8A7B14006FABE8 /* LatencyViewModel.swift in Sources */, D8C4A3392C8A7B14006FABE8 /* TransactionsList.swift in Sources */, D8C4A33A2C8A7B14006FABE8 /* DiscoverAssetsService.swift in Sources */, diff --git a/Gem/Core/Coordinator/WalletCoordinator+WalletConnector.swift b/Gem/Core/Coordinator/WalletCoordinator+WalletConnector.swift index ab144c7a..c9425fad 100644 --- a/Gem/Core/Coordinator/WalletCoordinator+WalletConnector.swift +++ b/Gem/Core/Coordinator/WalletCoordinator+WalletConnector.swift @@ -32,51 +32,30 @@ extension WalletCoordinator: WalletConnectorInteractable { } func sessionApproval(payload: WCPairingProposal) async throws -> WalletId { - return try await withCheckedThrowingContinuation { continuation in - let transferDataCallback = TransferDataCallback(payload: payload) { result in - switch result { - case let .success(value): - continuation.resume(with: .success(WalletId(id: value))) - case .failure(let error): - continuation.resume(throwing: error) - } - } - self.connectionProposal = transferDataCallback - } + try await walletConnectManager.handleApproveRequest( + payload: payload, + isPending: lockModel.shouldShowPlaceholder + ) } - + func signMessage(payload: SignMessagePayload) async throws -> String { - return try await withCheckedThrowingContinuation { continuation in - let signMessageCallback = TransferDataCallback(payload: payload) { result in - switch result { - case .success(let id): - continuation.resume(with: .success(id)) - case .failure(let error): - continuation.resume(throwing: error) - } - } - self.signMessage = signMessageCallback - } + try await walletConnectManager.handleSignMessageRequest( + payload: payload, + isPending: lockModel.shouldShowPlaceholder + ) } - + + func sendTransaction(transferData: WCTransferData) async throws -> String { + try await walletConnectManager.handleSendTransactionRequest( + payload: transferData, + isPending: lockModel.shouldShowPlaceholder + ) + } + func signTransaction(transferData: WCTransferData) async throws -> String { fatalError() } - - func sendTransaction(transferData: WCTransferData) async throws -> String { - return try await withCheckedThrowingContinuation { continuation in - let transferDataCallback = TransferDataCallback(payload: transferData) { result in - switch result { - case .success(let id): - continuation.resume(with: .success(id)) - case .failure(let error): - continuation.resume(throwing: error) - } - } - self.transferData = transferDataCallback - } - } - + func sendRawTransaction(transferData: WCTransferData) async throws -> String { fatalError() } diff --git a/Gem/Core/Coordinator/WalletCoordinator.swift b/Gem/Core/Coordinator/WalletCoordinator.swift index 703dcaf8..34722ade 100644 --- a/Gem/Core/Coordinator/WalletCoordinator.swift +++ b/Gem/Core/Coordinator/WalletCoordinator.swift @@ -15,7 +15,10 @@ struct WalletCoordinator: View { @State var navigationStateManager: NavigationStateManagable @ObservedObject var keystore: LocalKeystore = .main - + + @State var walletConnectManager: WalletConnectManager + @State var lockModel: LockSceneViewModel + let assetStore: AssetStore let balanceStore: BalanceStore let priceStore: PriceStore @@ -51,13 +54,8 @@ struct WalletCoordinator: View { let pricesTimer = Timer.publish(every: 600, tolerance: 1, on: .main, in: .common).autoconnect() - @State private var updateAvailableAlertSheetMessage: String? = .none @State var isPresentingError: String? = .none - @State var isPresentingWalletConnectBar: Bool = false - - @State var transferData: TransferDataCallback? // wallet connector - @State var signMessage: TransferDataCallback? // wallet connector - @State var connectionProposal: TransferDataCallback? // wallet connector + @State private var updateAvailableAlertSheetMessage: String? = .none init( db: DB @@ -147,12 +145,17 @@ struct WalletCoordinator: View { self.deviceService.observer() _navigationStateManager = State(initialValue: NavigationStateManager(initialSelecedTab: .wallet)) + _walletConnectManager = State(initialValue: WalletConnectManager( + connectionsService: connectionsService, + keystore: _keystore.wrappedValue) + ) + _lockModel = State(initialValue: LockSceneViewModel()) } var body: some View { VStack { if let currentWallet = keystore.currentWallet { - LockScreenScene(model: LockSceneViewModel()) { + LockScreenScene(model: lockModel) { MainTabView( model: .init(wallet: currentWallet), navigationStateManager: $navigationStateManager @@ -184,6 +187,13 @@ struct WalletCoordinator: View { WelcomeScene(model: WelcomeViewModel(keystore: keystore)) } } + .onChange(of: lockModel.shouldShowPlaceholder) { _, show in + if show { + walletConnectManager.setRequestsPending() + } else { + walletConnectManager.processPendingRequests() + } + } .onOpenURL(perform: { url in Task { await handleUrl(url: url) @@ -191,7 +201,7 @@ struct WalletCoordinator: View { //isPresentingError = url.absoluteString }) - .sheet(item: $transferData) { data in + .sheet(item: $walletConnectManager.transferData) { data in NavigationStack { ConfirmTransferScene( model: ConfirmTransferViewModel( @@ -208,8 +218,7 @@ struct WalletCoordinator: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(Localized.Common.cancel) { - transferData?.delegate(.failure(AnyError("User cancelled"))) - transferData = nil + walletConnectManager.cancelTransferData() } .bold() } @@ -217,7 +226,7 @@ struct WalletCoordinator: View { .navigationBarTitleDisplayMode(.inline) } } - .sheet(item: $signMessage) { data in + .sheet(item: $walletConnectManager.signMessage) { data in NavigationStack { SignMessageScene( model: SignMessageSceneViewModel( @@ -230,8 +239,7 @@ struct WalletCoordinator: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(Localized.Common.cancel) { - signMessage?.delegate(.failure(AnyError("User cancelled"))) - signMessage = nil + walletConnectManager.cancelSignMessage() } .bold() } @@ -239,7 +247,7 @@ struct WalletCoordinator: View { .navigationBarTitleDisplayMode(.inline) } } - .sheet(item: $connectionProposal) { data in + .sheet(item: $walletConnectManager.connectionProposal) { data in NavigationStack { ConnectionProposalScene( model: ConnectionProposalViewModel( @@ -253,8 +261,7 @@ struct WalletCoordinator: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(Localized.Common.cancel) { - connectionProposal?.delegate(.failure(ConnectionsError.userCancelled)) - connectionProposal = nil + walletConnectManager.cancelConnectionProposal() } .bold() } @@ -293,36 +300,27 @@ struct WalletCoordinator: View { runUpdatePrices() } .modifier( - ToastModifier(isPresenting: $isPresentingWalletConnectBar, value: "\(Localized.WalletConnect.brandName)...", systemImage: SystemImage.network) + ToastModifier( + isPresenting: $walletConnectManager.isPresentingWalletConnectBar, + value: "\(Localized.WalletConnect.brandName)...", + systemImage: SystemImage.network + ) ) } - - func runUpdatePrices() { + + private func runUpdatePrices() { NSLog("runUpdatePrices") Task { try await walletsService.updatePrices() } } - func handleUrl(url: URL) async { + private func handleUrl(url: URL) async { do { - let url = try URLParser.from(url: url) - //TODO: Show loading indicator of connecting to WC - switch url { - case .walletConnect(let uri): - isPresentingWalletConnectBar = true - try await connectionsService.addConnectionURI( - uri: uri, - wallet: try keystore.getCurrentWallet() - ) - case .walletConnectRequest: - isPresentingWalletConnectBar = true - break - } - + let urlAction = try URLParser.from(url: url) + try await walletConnectManager.handle(action: urlAction) } catch { NSLog("handleUrl error: \(error)") - isPresentingError = error.localizedDescription } } diff --git a/Gem/Core/Services/WalletConnectManager.swift b/Gem/Core/Services/WalletConnectManager.swift new file mode 100644 index 00000000..a64a2407 --- /dev/null +++ b/Gem/Core/Services/WalletConnectManager.swift @@ -0,0 +1,176 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import WalletConnector +import Keystore +import Primitives + +@Observable +class WalletConnectManager { + private let connectionsService: ConnectionsService + private let keystore: LocalKeystore + + private var pendingTransferDataRequest: TransferDataCallback? + private var pendingSignMessageRequest: TransferDataCallback? + private var pendingConnectionProposal: TransferDataCallback? + + var transferData: TransferDataCallback? + var signMessage: TransferDataCallback? + var connectionProposal: TransferDataCallback? + + var isPresentingWalletConnectBar: Bool = false + + init(connectionsService: ConnectionsService, keystore: LocalKeystore) { + self.connectionsService = connectionsService + self.keystore = keystore + } +} + +// MARK: - Business Logic + +extension WalletConnectManager { + func handleApproveRequest(payload: WCPairingProposal, isPending: Bool) async throws -> WalletId { + try await handleWalletConnectRequest( + payload: payload, + isPending: isPending, + mapResult: { WalletId(id: $0) }, + setCurrentRequest: { [weak self] transferDataCallback in + guard let `self` = self else { return } + connectionProposal = transferDataCallback + }, + setPendingRequest: { [weak self] transferDataCallback in + guard let `self` = self else { return } + pendingConnectionProposal = transferDataCallback + } + ) + } + + func handleSignMessageRequest(payload: SignMessagePayload, isPending: Bool) async throws -> String { + try await handleWalletConnectRequest( + payload: payload, + isPending: isPending, + mapResult: { $0 }, + setCurrentRequest: { [weak self] transferDataCallback in + guard let `self` = self else { return } + signMessage = transferDataCallback + }, + setPendingRequest: { [weak self] transferDataCallback in + guard let `self` = self else { return } + pendingSignMessageRequest = transferDataCallback + } + ) + } + + func handleSendTransactionRequest(payload: WCTransferData, isPending: Bool) async throws -> String { + return try await handleWalletConnectRequest( + payload: payload, + isPending: isPending, + mapResult: { $0 }, + setCurrentRequest: { [weak self] transferDataCallback in + guard let `self` = self else { return } + transferData = transferDataCallback + }, + setPendingRequest: { [weak self] transferDataCallback in + guard let `self` = self else { return } + pendingTransferDataRequest = transferDataCallback + } + ) + } + + func handle(action: URLAction) async throws { + switch action { + case .walletConnect(let uri): + isPresentingWalletConnectBar = true + try await connectionsService.addConnectionURI( + uri: uri, + wallet: try keystore.getCurrentWallet() + ) + case .walletConnectRequest: + isPresentingWalletConnectBar = false + // Handle specific WalletConnect requests if needed + } + } + + func cancelTransferData() { + transferData?.delegate(.failure(ConnectionsError.userCancelled)) + transferData = nil + } + + func cancelSignMessage() { + signMessage?.delegate(.failure(ConnectionsError.userCancelled)) + signMessage = nil + } + + func cancelConnectionProposal() { + connectionProposal?.delegate(.failure(ConnectionsError.userCancelled)) + connectionProposal = nil + } + + func setRequestsPending() { + if let currentTransferData = transferData { + pendingTransferDataRequest = currentTransferData + transferData = nil + } + + if let currentSignMessage = signMessage { + pendingSignMessageRequest = currentSignMessage + signMessage = nil + } + + if let currentConnectionProposal = connectionProposal { + pendingConnectionProposal = currentConnectionProposal + connectionProposal = nil + } + } + + func processPendingRequests() { + if let request = pendingTransferDataRequest { + transferData = request + pendingTransferDataRequest = nil + } + + if let request = pendingSignMessageRequest { + signMessage = request + pendingSignMessageRequest = nil + } + + if let proposal = pendingConnectionProposal { + connectionProposal = proposal + pendingConnectionProposal = nil + } + } + } + +// MARK: - Private + +extension WalletConnectManager { + private func handleWalletConnectRequest( + payload: T, + isPending: Bool, + mapResult: @escaping (String) -> ResultType, + setCurrentRequest: @escaping (TransferDataCallback) -> Void, + setPendingRequest: @escaping (TransferDataCallback) -> Void + ) async throws -> ResultType { + return try await withCheckedThrowingContinuation { continuation in + let delegate: ConfirmTransferDelegate = { result in + switch result { + case .success(let value): + continuation.resume(returning: mapResult(value)) + case .failure(let error): + continuation.resume(throwing: error) + } + } + + let transferDataCallback = TransferDataCallback( + payload: payload, + delegate: delegate + ) + + if isPending { + setPendingRequest(transferDataCallback) + } else { + setCurrentRequest(transferDataCallback) + } + } + } +}