From e49cf0115707a6e998c43a918f4a033af8c605bc Mon Sep 17 00:00:00 2001 From: ant013 Date: Mon, 11 Dec 2023 17:49:20 +0600 Subject: [PATCH] Handle toncoin deeplink address to open transfer tokens. Add toncoin address to donate screen --- .../project.pbxproj | 2 - .../Core/Address/AddressService.swift | 32 +++-- .../Core/Address/AddressUriParser.swift | 109 +++++++++++++----- .../Core/Managers/DeepLinkManager.swift | 14 +++ .../Core/Providers/AppConfig.swift | 1 + .../UnstoppableWallet/Info.plist | 18 ++- .../AddressAppShowModule.swift | 94 ++++++++------- .../RecipientAddressViewModel.swift | 4 +- .../Modules/Watch/WatchService.swift | 7 +- 9 files changed, 181 insertions(+), 100 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index b49e47eafb..b7ad642b65 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -9152,7 +9152,6 @@ 3A73FC9C258B1AF700FE4D34 /* MarketWatchlistViewController.swift in Sources */, 11B35B086B0D62A9D7A10CD0 /* AddTokenViewModel.swift in Sources */, 11B35A42BF19B93C6005FBD9 /* AddTokenService.swift in Sources */, - D0D90ADD2B1DA7DF0047C320 /* ScriptType.swift in Sources */, 11B35D550563934444558D15 /* AddTokenViewController.swift in Sources */, 58AAABE760739C0ECC6CA720 /* DownloadService.swift in Sources */, 11B3502E7AA00ACFE8EE8CD9 /* FormTextView.swift in Sources */, @@ -10819,7 +10818,6 @@ 11B35EF70120304F6D4F5561 /* MarketMultiSortHeaderView.swift in Sources */, D05E969A2A26278D002CCD71 /* TronApproveTransactionRecord.swift in Sources */, 2FA5D40FA7CAC1BA01C0B373 /* CoinOverviewViewModel.swift in Sources */, - D0D90ADC2B1DA7DF0047C320 /* ScriptType.swift in Sources */, 2FA5DBE42827FF3D114DBF4B /* CoinOverviewService.swift in Sources */, 2FA5DF4BB73C12C8DDF42599 /* CoinOverviewModule.swift in Sources */, D05E969D2A2627AF002CCD71 /* TronContractCallTransactionRecord.swift in Sources */, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressService.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressService.swift index 45c6cf689b..0f8755d42e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressService.swift @@ -167,25 +167,23 @@ extension AddressService { } func handleFetched(text: String) -> String { - let result = addressUriParser.parse(addressUri: text.trimmingCharacters(in: .whitespaces)) - switch result { - case .invalidBlockchainType: - showUriErrorRelay.accept(UriError.invalidBlockchainType) - return "" - case .invalidTokenType: - showUriErrorRelay.accept(UriError.invalidTokenType) - return "" - case .noUri, .wrongUri: - let text = text.trimmingCharacters(in: .whitespacesAndNewlines) - set(text: text) - return text - case let .uri(data): - - if let amount = data.amount { + do { + let result = try addressUriParser.parse(url: text.trimmingCharacters(in: .whitespaces)) + if let amount = result.amount { publishAmountRelay.accept(amount) } - set(text: data.address) - return data.address + set(text: result.address) + return result.address + } catch { + switch error { + case AddressUriParser.ParseError.noUri, AddressUriParser.ParseError.wrongUri: + let text = text.trimmingCharacters(in: .whitespacesAndNewlines) + set(text: text) + return text + default: + showUriErrorRelay.accept(error) + return "" + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift index f1709b5d3b..0c11ee0c95 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressUriParser.swift @@ -15,6 +15,38 @@ class AddressUriParser { return [prefix, s2].compactMap { $0 }.joined(separator: ":") } + private func parameters(from queryItems: [URLQueryItem]) -> (handled: [AddressUri.Field: String], unhandled: [String: String]) { + var handled = [AddressUri.Field: String]() + var unhandled = [String: String]() + + for item in queryItems { + guard let value = item.value else { continue } + if let field = AddressUri.Field(rawValue: item.name) { + handled[field] = value + } else { + unhandled[item.name] = value + } + } + + return (handled: handled, unhandled: unhandled) + } + + private func validate(parameters: [AddressUri.Field: String]) throws { + if let blockchainType, + let uid: String = parameters[.blockchainUid], + blockchainType != BlockchainType(uid: uid) + { + throw ParseError.invalidBlockchainType + } + + if let uid: String = parameters[.tokenUid], + let tokenType, + tokenType != TokenType(id: uid) + { + throw ParseError.invalidTokenType + } + } + private func fullAddress(scheme: String, address: String, uriBlockchainUid: String? = nil) -> String { // there is no explicit indication of the blockchain in the uri. We use the rules of the blockchain parser guard let uriBlockchainUid else { @@ -34,45 +66,54 @@ class AddressUriParser { return pair(BlockchainType(uid: uriBlockchainUid), address) } - func parse(addressUri: String) -> Result { - guard let components = URLComponents(string: addressUri), let scheme = components.scheme else { - return .noUri - } - if let validScheme = blockchainType?.uriScheme, components.scheme != validScheme { - return .invalidBlockchainType + private func handleDeepLink(url: String) -> AddressUri? { + guard let components = URLComponents(string: url), let scheme = components.scheme else { + return nil } - var uri = AddressUri(scheme: scheme) - guard let parameters = components.queryItems else { - uri.address = fullAddress(scheme: scheme, address: components.path) - return .uri(uri) - } + // try to parse ton deeplink + if scheme == DeepLinkManager.tonDeepLinkScheme, let tonScheme = BlockchainType.ton.uriScheme { + var uri = AddressUri(scheme: tonScheme) + uri.address = components.path.stripping(prefix: "/") - for parameter in parameters { - guard let value = parameter.value else { continue } - if let field = AddressUri.Field(rawValue: parameter.name) { - uri.parameters[field] = parameter.value - } else { - uri.unhandledParameters[parameter.name] = value + var params = parameters(from: components.queryItems ?? []) + if let amount = params.handled[.amount] { + params.handled[.amount] = TonAdapter.amount(kitAmount: amount).description } + params.handled[.blockchainUid] = BlockchainType.ton.uid + + uri.parameters = params.handled + uri.unhandledParameters = params.unhandled + + return uri } - if let uid: String = uri.value(field: .blockchainUid), - let blockchainType, - blockchainType != BlockchainType(uid: uid) - { - return .invalidBlockchainType + return nil + } + + func handleAddressUri(uri: String) throws -> AddressUri { + guard let components = URLComponents(string: uri), let scheme = components.scheme else { + throw ParseError.noUri } - if let uid: String = uri.value(field: .tokenUid), - let tokenType, - tokenType != TokenType(id: uid) - { - return .invalidTokenType + if let validScheme = blockchainType?.uriScheme, components.scheme != validScheme { + throw ParseError.invalidBlockchainType } + var uri = AddressUri(scheme: scheme) + guard let items = components.queryItems else { + uri.address = fullAddress(scheme: scheme, address: components.path) + return uri + } + + let params = parameters(from: items) + try validate(parameters: params.handled) + + uri.parameters = params.handled + uri.unhandledParameters = params.unhandled uri.address = fullAddress(scheme: scheme, address: components.path, uriBlockchainUid: uri.parameters[.blockchainUid]) - return .uri(uri) + + return uri } static func hasUriPrefix(text: String) -> Bool { @@ -81,6 +122,15 @@ class AddressUriParser { } extension AddressUriParser { + func parse(url: String) throws -> AddressUri { + // check if we try to parse deeplink address (like ton://transfer/
) + if let addressUri = handleDeepLink(url: url) { + return addressUri + } + + return try handleAddressUri(uri: url) + } + func uri(_ addressUri: AddressUri) -> String { var components = URLComponents() components.scheme = blockchainType?.uriScheme @@ -100,12 +150,11 @@ extension AddressUriParser { } extension AddressUriParser { - enum Result { + enum ParseError: Error { case wrongUri case invalidBlockchainType case invalidTokenType case noUri - case uri(AddressUri) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift index a412541e59..d50a9b2015 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift @@ -1,8 +1,11 @@ +import ComponentKit import Foundation import RxRelay import RxSwift class DeepLinkManager { + static let tonDeepLinkScheme = "ton" + private let newSchemeRelay = BehaviorRelay(value: nil) } @@ -28,6 +31,16 @@ extension DeepLinkManager { return true } + if scheme == Self.tonDeepLinkScheme { + let parser = AddressParserFactory.parser(blockchainType: .ton, tokenType: nil) + do { + let address = try parser.parse(url: url.absoluteString) + newSchemeRelay.accept(.transfer(addressUri: address)) + } catch { + HudHelper.instance.show(banner: .error(string: error.localizedDescription)) + } + } + if scheme == "unstoppable.money", host == "coin" { let uid = path.replacingOccurrences(of: "/", with: "") @@ -43,5 +56,6 @@ extension DeepLinkManager { enum DeepLink { case walletConnect(url: String) case coin(uid: String) + case transfer(addressUri: AddressUri) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift index 4f2c1621a1..8054f20706 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift @@ -35,6 +35,7 @@ enum AppConfig { .arbitrumOne: "0xA24c159C7f1E4A04dab7c364C2A8b87b3dBa4cd1", .gnosis: "0xA24c159C7f1E4A04dab7c364C2A8b87b3dBa4cd1", .fantom: "0xA24c159C7f1E4A04dab7c364C2A8b87b3dBa4cd1", + .ton: "UQAYLATDlfKgn3cKZAgznvowhXzpqgxrIicesxJfo9f6PN3k", .tron: "TQzANCd363w5CjRWDtswm8Y5nFPAdnwekF", .solana: "5gattKnvu5f1NDHBuZ6VfDXjRrJa9UcAArkZ3ys3e82F", ] diff --git a/UnstoppableWallet/UnstoppableWallet/Info.plist b/UnstoppableWallet/UnstoppableWallet/Info.plist index 6094b15304..ace807c626 100644 --- a/UnstoppableWallet/UnstoppableWallet/Info.plist +++ b/UnstoppableWallet/UnstoppableWallet/Info.plist @@ -2,10 +2,10 @@ - BlockchairApiKey - ${blockchair_api_key} ArbiscanApiKey ${arbiscan_api_key} + BlockchairApiKey + ${blockchair_api_key} BscscanApiKey ${bscscan_api_key} CFBundleDevelopmentRegion @@ -34,6 +34,16 @@ unstoppable.money + + CFBundleTypeRole + Editor + CFBundleURLName + toncoin.deeplink + CFBundleURLSchemes + + ton + + CFBundleVersion $(CURRENT_PROJECT_VERSION) @@ -103,8 +113,6 @@ OfficeMode ${OfficeMode} - oneInchApiKey - ${one_inch_api_key} OpenSeaApiKey ${open_sea_api_key} OptimismEtherscanApiKey @@ -154,5 +162,7 @@ ${unstoppable_domains_api_key} WallectConnectV2ProjectKey ${wallet_connect_v2_project_key} + oneInchApiKey + ${one_inch_api_key} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift index e653acf31c..5316569566 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift @@ -16,18 +16,27 @@ class AddressAppShowModule { } let abstractParser = AddressUriParser(blockchainType: nil, tokenType: nil) - let result = abstractParser.parse(addressUri: text) - switch result { - case let .uri(uri): - guard BlockchainType.supported.map(\.uriScheme).contains(uri.scheme) else { + do { + let addressUri = try abstractParser.parse(url: text) + guard BlockchainType.supported.map(\.uriScheme).contains(addressUri.scheme) else { return nil } - return uri - default: return nil + return addressUri + } catch { + return nil } } - private func showSendTokenList(uri: AddressUri, allowedBlockchainTypes: [BlockchainType]?, allowedTokenTypes: [TokenType]?) { + private func showSendTokenList(uri: AddressUri, allowedBlockchainTypes: [BlockchainType]? = nil) { + let allowedBlockchainTypes = allowedBlockchainTypes ?? uri.allowedBlockchainTypes + + var allowedTokenTypes: [TokenType]? + if let tokenUid: String = uri.value(field: .tokenUid), + let tokenType = TokenType(id: tokenUid) + { + allowedTokenTypes = [tokenType] + } + guard let viewController = WalletModule.sendTokenListViewController( allowedBlockchainTypes: allowedBlockchainTypes, allowedTokenTypes: allowedTokenTypes, @@ -42,47 +51,48 @@ class AddressAppShowModule { extension AddressAppShowModule: IEventHandler { @MainActor func handle(event: Any, eventType: EventHandler.EventType) async throws { - guard eventType.contains(.address) else { - return - } - - guard var text = event as? String else { - throw EventHandler.HandleError.noSuitableHandler + // check if we parse deeplink with transfer address + if eventType.contains(.deepLink) { + if let event = event as? DeepLinkManager.DeepLink { + guard case let .transfer(parsed) = event else { + throw EventHandler.HandleError.noSuitableHandler + } + showSendTokenList(uri: parsed) + } else { + return + } } - text = text.trimmingCharacters(in: .whitespacesAndNewlines) - - // Handle uri string if exist - if let uri = uri(text: text) { - let allowedBlockchainTypes = uri.allowedBlockchainTypes - var allowedTokenTypes: [TokenType]? - if let tokenUid: String = uri.value(field: .tokenUid), - let tokenType = TokenType(id: tokenUid) - { - allowedTokenTypes = [tokenType] + // check if we parse text address or uri + if eventType.contains(.address) { + guard let text = event as? String else { + throw EventHandler.HandleError.noSuitableHandler } - showSendTokenList(uri: uri, allowedBlockchainTypes: allowedBlockchainTypes, allowedTokenTypes: allowedTokenTypes) - } else { - let disposeBag = DisposeBag() - let chain = AddressParserFactory.parserChain(blockchainType: nil, withEns: false) - let types = try await withCheckedThrowingContinuation { continuation in - chain - .handlers(address: text) - .subscribe(onSuccess: { items in - continuation.resume(returning: items.map(\.blockchainType)) - }, onError: { error in - continuation.resume(throwing: error) - }) - .disposed(by: disposeBag) - } + if let parsed = uri(text: text.trimmingCharacters(in: .whitespacesAndNewlines)) { + showSendTokenList(uri: parsed) + } else { + let disposeBag = DisposeBag() + let chain = AddressParserFactory.parserChain(blockchainType: nil, withEns: false) + let types = try await withCheckedThrowingContinuation { continuation in + chain + .handlers(address: text) + .subscribe(onSuccess: { items in + continuation.resume(returning: items.map(\.blockchainType)) + }, onError: { error in + continuation.resume(throwing: error) + }) + .disposed(by: disposeBag) + } - guard !types.isEmpty else { - throw EventHandler.HandleError.noSuitableHandler + guard !types.isEmpty else { + throw EventHandler.HandleError.noSuitableHandler + } + var uri = AddressUri(scheme: "") + uri.address = text + showSendTokenList(uri: uri, allowedBlockchainTypes: types) + return } - var uri = AddressUri(scheme: "") - uri.address = text - showSendTokenList(uri: uri, allowedBlockchainTypes: types, allowedTokenTypes: nil) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/RecipientAddressViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/RecipientAddressViewModel.swift index d8a6d83d51..490d0e0d19 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/RecipientAddressViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/RecipientAddressViewModel.swift @@ -159,13 +159,15 @@ extension AddressService.AddressError: LocalizedError { } } -extension AddressService.UriError: LocalizedError { +extension AddressUriParser.ParseError: LocalizedError { var errorDescription: String? { switch self { case .invalidBlockchainType: return "send.error.invalid_blockchain".localized case .invalidTokenType: return "send.error.invalid_token".localized + case .wrongUri, .noUri: + return "alert.cant_recognize".localized } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchService.swift index 53984d8639..029d26cae3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchService.swift @@ -65,10 +65,9 @@ class WatchService { } private func parseUri(text: String) { - switch uriParser.parse(addressUri: text) { - case let .uri(data): - parseAddress(text: data.address) - default: + if let address = try? uriParser.parse(url: text) { + parseAddress(text: address.address) + } else { parseAddress(text: text) } }