From bb5d56ee75f90815691b86e755f26f23c9bf5374 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Thu, 28 Nov 2024 17:30:03 +0700 Subject: [PATCH 1/2] Update ton-connect. Handle links and deeplink --- .../Core/Address/AddressUriParser.swift | 2 +- .../Core/Managers/DeepLinkManager.swift | 10 ++++ .../UnstoppableWallet/Info.plist | 1 + .../Modules/Main/MainModule.swift | 2 +- .../Main/MainSettingsViewController.swift | 18 +++---- .../Modules/TonConnect/TonConnect.swift | 6 +-- .../TonConnect/TonConnectConnectView.swift | 8 ++- .../TonConnectConnectViewModel.swift | 4 +- .../TonConnect/TonConnectEventHandler.swift | 21 ++++++-- .../TonConnect/TonConnectManager.swift | 49 +++++++++++++------ .../TonConnect/TonConnectParameters.swift | 4 +- 11 files changed, 85 insertions(+), 40 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift index 0c11ee0c95..2c55050f77 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift @@ -72,7 +72,7 @@ class AddressUriParser { } // try to parse ton deeplink - if scheme == DeepLinkManager.tonDeepLinkScheme, let tonScheme = BlockchainType.ton.uriScheme { + if scheme == DeepLinkManager.deepLinkScheme, let tonScheme = BlockchainType.ton.uriScheme { var uri = AddressUri(scheme: tonScheme) uri.address = components.path.stripping(prefix: "/") diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift index ac0d45f8a2..7adf4ce638 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift @@ -7,6 +7,8 @@ import RxSwift class DeepLinkManager { static let deepLinkScheme = "unstoppable.money" static let tonDeepLinkScheme = "ton" + static let tonUniversalHost = "ton-connect" + static let tonDeepLinkHost = "tc" private let newSchemeRelay = BehaviorRelay(value: nil) } @@ -33,6 +35,13 @@ extension DeepLinkManager { return true } + if ((scheme == DeepLinkManager.deepLinkScheme && (host == Self.tonDeepLinkHost || host == Self.tonUniversalHost)) || + (scheme == "https" && host == Self.deepLinkScheme && path == "/\(Self.tonUniversalHost)")), + let parameters = try? TonConnectManager.parseParameters(queryItems: queryItems) { + newSchemeRelay.accept(.tonConnect(parameters: parameters)) + return true + } + if scheme == Self.tonDeepLinkScheme { let parser = AddressParserFactory.parser(blockchainType: .ton, tokenType: nil) do { @@ -74,6 +83,7 @@ extension DeepLinkManager { extension DeepLinkManager { enum DeepLink { case walletConnect(url: String) + case tonConnect(parameters: TonConnectParameters) case coin(uid: String) case transfer(addressUri: AddressUri) case referral(telegramUserId: String, referralCode: String) diff --git a/UnstoppableWallet/UnstoppableWallet/Info.plist b/UnstoppableWallet/UnstoppableWallet/Info.plist index 652b421002..2c801cddb9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Info.plist +++ b/UnstoppableWallet/UnstoppableWallet/Info.plist @@ -69,6 +69,7 @@ LSApplicationQueriesSchemes + googlechrome tg twitter cydia diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift index d8871d5793..cfbceefda3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift @@ -54,7 +54,7 @@ enum MainModule { let tonConnectHandler = TonConnectEventHandler(parentViewController: viewController) eventHandler.append(handler: deepLinkHandler) - // eventHandler.append(handler: tonConnectHandler) + eventHandler.append(handler: tonConnectHandler) eventHandler.append(handler: widgetCoinHandler) eventHandler.append(handler: sendAddressHandler) eventHandler.append(handler: telegramUserHandler) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index a03165b29f..8a5743ed6a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -262,15 +262,15 @@ class MainSettingsViewController: ThemeViewController { self?.viewModel.onTapWalletConnect() } ), - // StaticRow( - // cell: tonConnectCell, - // id: "ton-connect", - // height: .heightCell48, - // autoDeselect: true, - // action: { [weak self] in - // self?.onTapTonConnect() - // } - // ), + StaticRow( + cell: tonConnectCell, + id: "ton-connect", + height: .heightCell48, + autoDeselect: true, + action: { [weak self] in + self?.onTapTonConnect() + } + ), tableView.universalRow48( id: "backup-manager", image: .local(UIImage(named: "icloud_24")), diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift index cf93b9ff19..fdc6443da0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift @@ -14,10 +14,8 @@ enum TonConnect { struct DeviceInfo: Encodable { let platform = "iphone" - let appName = "Tonkeeper" - let appVersion = "3.4.0" - // let appName = AppConfig.appName - // let appVersion = AppConfig.appVersion + let appName = "Unstoppable Wallet" + let appVersion = AppConfig.appVersion let maxProtocolVersion = 2 let features = [ FeatureCompatible.legacy(Feature()), diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift index 63f013d45a..d85386c029 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift @@ -7,8 +7,8 @@ struct TonConnectConnectView: View { @State private var selectAccountPresented = false - init(config: TonConnectConfig) { - _viewModel = StateObject(wrappedValue: TonConnectConnectViewModel(config: config)) + init(config: TonConnectConfig, returnDeepLink: String? = nil) { + _viewModel = StateObject(wrappedValue: TonConnectConnectViewModel(config: config, returnDeepLink: returnDeepLink)) } var body: some View { @@ -91,6 +91,10 @@ struct TonConnectConnectView: View { } .onReceive(viewModel.finishPublisher) { presentationMode.wrappedValue.dismiss() + + if let deeplink = viewModel.returnDeepLink, let url = URL(string: deeplink), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } } .navigationTitle("TON Connect") .navigationBarTitleDisplayMode(.inline) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift index ff0c0b9487..f3ab97c907 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift @@ -4,6 +4,7 @@ import Foundation class TonConnectConnectViewModel: ObservableObject { private let parameters: TonConnectParameters let manifest: TonConnectManifest + let returnDeepLink: String? private let tonConnectManager = App.shared.tonConnectManager private let accountManager = App.shared.accountManager @@ -13,9 +14,10 @@ class TonConnectConnectViewModel: ObservableObject { private let finishSubject = PassthroughSubject() - init(config: TonConnectConfig) { + init(config: TonConnectConfig, returnDeepLink: String?) { parameters = config.parameters manifest = config.manifest + self.returnDeepLink = returnDeepLink eligibleAccounts = accountManager.accounts.filter(\.type.supportsTonConnect).sorted { $0.name < $1.name } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift index 3ae6bbe06d..9ea4f8f0c3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift @@ -24,14 +24,25 @@ class TonConnectEventHandler { extension TonConnectEventHandler: IEventHandler { func handle(source _: StatPage, event: Any, eventType _: EventHandler.EventType) async throws { - guard let deeplink = event as? String else { - throw EventHandler.HandleError.noSuitableHandler + var config: TonConnectConfig? + let returnDeepLink: String? + + if case let .tonConnect(parameters) = event as? DeepLinkManager.DeepLink { + config = try await tonConnectManager.loadTonConnectConfiguration(parameters: parameters) + returnDeepLink = parameters.returnDeepLink + } else if let deeplink = event as? String { + config = try await tonConnectManager.loadTonConnectConfiguration(deeplink: deeplink) + returnDeepLink = nil + } else { + returnDeepLink = nil } - let config = try await tonConnectManager.loadTonConnectConfiguration(deeplink: deeplink) - + guard let config = config else { + throw EventHandler.HandleError.noSuitableHandler + } + await MainActor.run { [weak self] in - let view = TonConnectConnectView(config: config) + let view = TonConnectConnectView(config: config, returnDeepLink: returnDeepLink) self?.parentViewController?.visibleController.present(view.toViewController(), animated: true) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift index 4b0115d3fc..dff7efe587 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift @@ -27,7 +27,7 @@ class TonConnectManager { configuration.timeoutIntervalForResource = TimeInterval(Int.max) apiClient = TonConnectAPI.Client( - serverURL: (try? TonConnectAPI.Servers.server1()) ?? URL(string: "https://bridge.tonapi.io/bridge")!, + serverURL: URL(string: "https://bridge.unstoppable.money/bridge")!, transport: StreamURLSessionTransport(urlSessionConfiguration: configuration), middlewares: [] ) @@ -165,19 +165,12 @@ class TonConnectManager { guard let url = URL(string: deeplink), let components = URLComponents(url: url, resolvingAgainstBaseURL: true), - components.scheme == .tcScheme, - let queryItems = components.queryItems, - let versionValue = queryItems.first(where: { $0.name == .versionKey })?.value, - let version = TonConnectParameters.Version(rawValue: versionValue), - let clientId = queryItems.first(where: { $0.name == .clientIdKey })?.value, - let requestPayloadValue = queryItems.first(where: { $0.name == .requestPayloadKey })?.value, - let requestPayloadData = requestPayloadValue.data(using: .utf8), - let requestPayload = try? JSONDecoder().decode(TonConnectRequestPayload.self, from: requestPayloadData) + components.scheme == .httpsScheme || components.scheme == .tcScheme else { throw ServiceError.incorrectUrl } - - return TonConnectParameters(version: version, clientId: clientId, requestPayload: requestPayload) + + return try TonConnectManager.parseParameters(queryItems: components.queryItems) } private func loadManifest(url: URL) async throws -> TonConnectManifest { @@ -195,11 +188,11 @@ class TonConnectManager { let encrypted = try sessionCrypto.encrypt(message: encoded, receiverPublicKey: receiverPublicKey) - _ = try await apiClient.message( + let _ = try await apiClient.message( query: .init(client_id: sessionCrypto.sessionId, to: clientId, ttl: 300), body: .plainText(.init(stringLiteral: encrypted.base64EncodedString())) ) - + // _ = try resp.ok.body.json } @@ -220,9 +213,7 @@ extension TonConnectManager { sendTransactionRequestSubject.eraseToAnyPublisher() } - func loadTonConnectConfiguration(deeplink: String) async throws -> TonConnectConfig { - let parameters = try parseTonConnect(deeplink: deeplink) - + func loadTonConnectConfiguration(parameters: TonConnectParameters) async throws -> TonConnectConfig { do { let manifest = try await loadManifest(url: parameters.requestPayload.manifestUrl) return TonConnectConfig(parameters: parameters, manifest: manifest) @@ -231,6 +222,12 @@ extension TonConnectManager { } } + func loadTonConnectConfiguration(deeplink: String) async throws -> TonConnectConfig { + let parameters = try parseTonConnect(deeplink: deeplink) + + return try await loadTonConnectConfiguration(parameters: parameters) + } + func connect(account: Account, parameters: TonConnectParameters, manifest: TonConnectManifest) async throws { let (publicKey, secretKey) = try TonKitManager.keyPair(accountType: account.type) @@ -271,6 +268,24 @@ extension TonConnectManager { } } +extension TonConnectManager { + static func parseParameters(queryItems: [URLQueryItem]?) throws -> TonConnectParameters { + guard let queryItems = queryItems, + let versionValue = queryItems.first(where: { $0.name == .versionKey })?.value, + let version = TonConnectParameters.Version(rawValue: versionValue), + let clientId = queryItems.first(where: { $0.name == .clientIdKey })?.value, + let requestPayloadValue = queryItems.first(where: { $0.name == .requestPayloadKey })?.value, + let requestPayloadData = requestPayloadValue.data(using: .utf8), + let requestPayload = try? JSONDecoder().decode(TonConnectRequestPayload.self, from: requestPayloadData) + else { + throw ServiceError.incorrectUrl + } + + let returnDeepLink = queryItems.first(where: { $0.name == .returnDeepLink })?.value + return TonConnectParameters(version: version, clientId: clientId, requestPayload: requestPayload, ret: returnDeepLink) + } +} + extension TonConnectManager { enum ServiceError: Error { case incorrectUrl @@ -291,9 +306,11 @@ extension TonConnectManager { private extension String { static let tcScheme = "tc" + static let httpsScheme = "https" static let versionKey = "v" static let clientIdKey = "id" static let requestPayloadKey = "r" + static let returnDeepLink = "ret" } struct TonConnectSendTransactionRequest { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift index 3bdb161879..ccb591bf29 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift @@ -2,11 +2,13 @@ struct TonConnectParameters { let version: Version let clientId: String let requestPayload: TonConnectRequestPayload + let returnDeepLink: String? - init(version: Version, clientId: String, requestPayload: TonConnectRequestPayload) { + init(version: Version, clientId: String, requestPayload: TonConnectRequestPayload, ret: String? = nil) { self.version = version self.clientId = clientId self.requestPayload = requestPayload + self.returnDeepLink = ret } } From 0e99af36d2de89d5438783cf7241fd279109ebca Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Mon, 2 Dec 2024 17:22:02 +0700 Subject: [PATCH 2/2] Add http-body parser for different bridges --- .../Sources/TonConnectAPI/Client.swift | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift b/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift index 4212617dcd..8bc2d16ab2 100644 --- a/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift +++ b/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift @@ -134,19 +134,19 @@ public struct Client: APIProtocol { switch response.status.code { case 200: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) - let body: Components.Responses.Response.Body + let bodyPayload: BodyUniversalData if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try await converter.getResponseBodyAsJSON( - Components.Responses.Response.Body.jsonPayload.self, + bodyPayload = try await converter.getResponseBodyAsJSON( + JsonPayload.self, from: responseBody, transforming: { value in .json(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } - return .ok(.init(body: body)) + return .ok(.init(body: .json(try bodyPayload.json))) default: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Components.Responses.Response.Body @@ -167,3 +167,41 @@ public struct Client: APIProtocol { ) } } + +enum BodyUniversalData: Sendable, Hashable { + + case json(JsonPayload) + public var json: Components.Responses.Response.Body.jsonPayload { + get throws { + switch self { + case let .json(body): + guard let message = body.message ?? body.status else { + throw ParsingError.cantParseBody + } + return .init(message: message, statusCode: body.statusCode ?? 200) + } + } + } +} + +public struct JsonPayload: Codable, Hashable, Sendable { + public var message: String? + public var status: String? + public var statusCode: Int64? + + public init(status: Swift.String?, message: String?, statusCode: Int64?) { + self.status = status + self.message = message + self.statusCode = statusCode + } + + public enum CodingKeys: String, CodingKey { + case status + case message + case statusCode + } +} + +public enum ParsingError: Error { + case cantParseBody +}