From d40fb9fd709777c33923442fe0735bb5ebe79a98 Mon Sep 17 00:00:00 2001 From: ChrisBenua Date: Thu, 6 Feb 2025 23:09:57 +0300 Subject: [PATCH 1/7] [trello.com/c/0dgGzsPE]: timeouts for sending messages/attachments --- Adamant.xcodeproj/project.pbxproj | 6 ++ Adamant/App/DI/AppAssembly.swift | 3 +- .../AdmWalletService+DynamicConstants.swift | 4 + .../Adamant/AdmWalletService+Timeouts.swift | 16 ++++ ...AdamantPushNotificationsTokenService.swift | 2 +- .../DataProviders/AdamantChatsProvider.swift | 18 ++++- .../AdamantTransfersProvider.swift | 5 +- CommonKit/Scripts/CoinsScript.rb | 20 +++++ .../Localization/de.lproj/Localizable.strings | 3 + .../Localization/en.lproj/Localizable.strings | 3 + .../Localization/ru.lproj/Localizable.strings | 3 + .../Localization/zh.lproj/Localizable.strings | 3 + .../Sources/CommonKit/Helpers/Deadline.swift | 76 +++++++++++++++++++ .../Localization/AdamantLocalized.swift | 3 + .../CommonKit/Models/ApiServiceError.swift | 6 +- .../Protocols/AdamantApiServiceProtocol.swift | 6 +- .../ApiService/AdamantApi+Chats.swift | 6 +- .../ApiService/AdamantApi+Transactions.swift | 5 +- .../ApiService/AdamantApiService.swift | 27 ++++++- .../HealthCheck/HealthCheckWrapper.swift | 27 +++++++ 20 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 Adamant/Modules/Wallets/Adamant/AdmWalletService+Timeouts.swift create mode 100644 CommonKit/Sources/CommonKit/Helpers/Deadline.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 8a5bdec24..b70121e4e 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -489,6 +489,9 @@ AA8FFFDA2D50DFB2001D8576 /* dash_unspent_transaction_unit_test.json in Resources */ = {isa = PBXBuildFile; fileRef = AA8FFFD92D50DFA3001D8576 /* dash_unspent_transaction_unit_test.json */; }; AA8FFFDC2D50E079001D8576 /* dash_unverified_unspent_transactions.json in Resources */ = {isa = PBXBuildFile; fileRef = AA8FFFDB2D50E063001D8576 /* dash_unverified_unspent_transactions.json */; }; AA8FFFE12D50E175001D8576 /* dash_send_transaction_unit_response.json in Resources */ = {isa = PBXBuildFile; fileRef = AA8FFFDF2D50E13F001D8576 /* dash_send_transaction_unit_response.json */; }; + AA8FFFCA2D4E6435001D8576 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AA8FFFC92D4E6435001D8576 /* CryptoSwift */; }; + AA8FFFCC2D50D503001D8576 /* NodeOrigin+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8FFFCB2D50D4F8001D8576 /* NodeOrigin+Extensions.swift */; }; + AA8FFFE72D5543C1001D8576 /* AdmWalletService+Timeouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8FFFE62D5543BA001D8576 /* AdmWalletService+Timeouts.swift */; }; AAB01CAD2D3AE44B007D6BF4 /* BitcoinKitTransactionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CAC2D3AE449007D6BF4 /* BitcoinKitTransactionFactory.swift */; }; AAB01CAF2D3AECED007D6BF4 /* DogeWalletServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CAE2D3AECE6007D6BF4 /* DogeWalletServiceTests.swift */; }; AAB01CB12D3AF01B007D6BF4 /* DogeApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CB02D3AF015007D6BF4 /* DogeApiServiceProtocol.swift */; }; @@ -1183,6 +1186,7 @@ AA8FFFD92D50DFA3001D8576 /* dash_unspent_transaction_unit_test.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = dash_unspent_transaction_unit_test.json; sourceTree = ""; }; AA8FFFDB2D50E063001D8576 /* dash_unverified_unspent_transactions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = dash_unverified_unspent_transactions.json; sourceTree = ""; }; AA8FFFDF2D50E13F001D8576 /* dash_send_transaction_unit_response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = dash_send_transaction_unit_response.json; sourceTree = ""; }; + AA8FFFE62D5543BA001D8576 /* AdmWalletService+Timeouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdmWalletService+Timeouts.swift"; sourceTree = ""; }; AAB01CAC2D3AE449007D6BF4 /* BitcoinKitTransactionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinKitTransactionFactory.swift; sourceTree = ""; }; AAB01CAE2D3AECE6007D6BF4 /* DogeWalletServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletServiceTests.swift; sourceTree = ""; }; AAB01CB02D3AF015007D6BF4 /* DogeApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeApiServiceProtocol.swift; sourceTree = ""; }; @@ -2860,6 +2864,7 @@ E94008902119D22400CD2D67 /* Adamant */ = { isa = PBXGroup; children = ( + AA8FFFE62D5543BA001D8576 /* AdmWalletService+Timeouts.swift */, 93294B852AAD0E0A00911109 /* AdmWallet.swift */, 93294B862AAD0E0A00911109 /* AdmWalletService.swift */, E993301F21354B1800CD5200 /* AdmWalletFactory.swift */, @@ -3833,6 +3838,7 @@ E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */, E90847332196FEA80095825D /* TransferTransaction+CoreDataProperties.swift in Sources */, 9366588D2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift in Sources */, + AA8FFFE72D5543C1001D8576 /* AdmWalletService+Timeouts.swift in Sources */, AAFB3CAB2D3997DD000CCCE9 /* EthApiServiceProtocol.swift in Sources */, E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */, 3AA6DF462BA9BEB700EA2E16 /* MediaContentView.swift in Sources */, diff --git a/Adamant/App/DI/AppAssembly.swift b/Adamant/App/DI/AppAssembly.swift index 02cdbda2e..9032dcfca 100644 --- a/Adamant/App/DI/AppAssembly.swift +++ b/Adamant/App/DI/AppAssembly.swift @@ -351,7 +351,8 @@ struct AppAssembly: MainThreadAssembly { accountsProvider: r.resolve(AccountsProvider.self)!, transactionService: r.resolve(ChatTransactionService.self)!, securedStore: r.resolve(SecuredStore.self)!, - walletServiceCompose: r.resolve(WalletServiceCompose.self)! + walletServiceCompose: r.resolve(WalletServiceCompose.self)!, + timeouts: AdmWalletService.timeouts ) }.inObjectScope(.container) diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift index fa2635674..17182ea42 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift @@ -31,6 +31,10 @@ extension AdmWalletService { 4000 } + static var timeouts: MessageTimeouts { + MessageTimeouts(message: 300, attachment: 300) + } + var tokenName: String { "ADAMANT Messenger" } diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+Timeouts.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+Timeouts.swift new file mode 100644 index 000000000..edb5f91ac --- /dev/null +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+Timeouts.swift @@ -0,0 +1,16 @@ +// +// AdmWalletService+Timeouts.swift +// Adamant +// +// Created by Christian Benua on 06.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation + +extension AdmWalletService { + struct MessageTimeouts { + let message: TimeInterval + let attachment: TimeInterval + } +} diff --git a/Adamant/Services/AdamantPushNotificationsTokenService.swift b/Adamant/Services/AdamantPushNotificationsTokenService.swift index 0e673ba66..3b3d9b2b9 100644 --- a/Adamant/Services/AdamantPushNotificationsTokenService.swift +++ b/Adamant/Services/AdamantPushNotificationsTokenService.swift @@ -190,7 +190,7 @@ private extension AdamantPushNotificationsTokenService { ) else { return nil } Task { - switch await apiService.sendMessageTransaction(transaction: messageTransaction) { + switch await apiService.sendMessageTransaction(transaction: messageTransaction, timeout: nil) { case .success: completion(true) case .failure: diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 02e06cbc2..1062dbf5e 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -20,6 +20,7 @@ actor AdamantChatsProvider: ChatsProvider { private let adamantCore: AdamantCore private let transactionService: ChatTransactionService private let walletServiceCompose: WalletServiceCompose + private let timeouts: AdmWalletService.MessageTimeouts let accountService: AccountService let accountsProvider: AccountsProvider @@ -80,7 +81,8 @@ actor AdamantChatsProvider: ChatsProvider { accountsProvider: AccountsProvider, transactionService: ChatTransactionService, securedStore: SecuredStore, - walletServiceCompose: WalletServiceCompose + walletServiceCompose: WalletServiceCompose, + timeouts: AdmWalletService.MessageTimeouts ) { self.accountService = accountService self.apiService = apiService @@ -91,6 +93,7 @@ actor AdamantChatsProvider: ChatsProvider { self.transactionService = transactionService self.securedStore = securedStore self.walletServiceCompose = walletServiceCompose + self.timeouts = timeouts Task { await setupSecuredStore() @@ -1286,7 +1289,12 @@ extension AdamantChatsProvider { transaction.transactionId = locallyID transaction.chatMessageId = locallyID - let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() + let id = try await apiService.sendMessageTransaction( + transaction: signedTransaction, + timeout: transaction.isFileTransfer + ? timeouts.attachment + : timeouts.message + ).get() // Update ID with recieved, add to unconfirmed transactions. transaction.transactionId = String(id) @@ -1980,4 +1988,10 @@ extension AdamantChatsProvider { } } +extension ChatTransaction { + var isFileTransfer: Bool { + (self as? RichMessageTransaction)?.additionalType == RichAdditionalType.file + } +} + private let requestRepeatDelay: TimeInterval = 2 diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index 2da3e9b9b..32b2050ae 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -522,7 +522,10 @@ extension AdamantTransfersProvider { } do { - let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() + let id = try await apiService.sendMessageTransaction( + transaction: signedTransaction, + timeout: nil + ).get() transaction.transactionId = String(id) await chatsProvider?.addUnconfirmed(transactionId: id, managedObjectId: transaction.objectID) diff --git a/CommonKit/Scripts/CoinsScript.rb b/CommonKit/Scripts/CoinsScript.rb index 3a94f2bea..2ef695b35 100755 --- a/CommonKit/Scripts/CoinsScript.rb +++ b/CommonKit/Scripts/CoinsScript.rb @@ -174,6 +174,17 @@ def writeToSwiftFile(name, json) oldPendingAttempts = txFetchInfo["oldPendingAttempts"] end + # timeouts + timeouts = json["timeout"] + + messageTimeout = nil + attachmentTimeout = nil + + if !timeouts.nil? + messageTimeout = timeouts["message"] / 1000 + attachmentTimeout = timeouts["attachment"] / 1000 + end + # Gas for eth reliabilityGasPricePercent = json["reliabilityGasPricePercent"] reliabilityGasLimitPercent = json["reliabilityGasLimitPercent"] @@ -228,6 +239,15 @@ def writeToSwiftFile(name, json) emptyText } +#{timeouts ? + createSwiftVariable( + "timeouts", + "MessageTimeouts(message: #{messageTimeout}, attachment: #{attachmentTimeout})", + "MessageTimeouts", + true + ) : emptyText +} + #{reliabilityGasPricePercent ? createSwiftVariable("reliabilityGasPricePercent", reliabilityGasPricePercent, "BigUInt", false) : emptyText diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings index 02bec70dd..e47aa3241 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings @@ -571,6 +571,9 @@ /* Shared error: Network problems. In most cases - no connection */ "Error.NoNetwork" = "Keine Verbindung"; +/* Shared error: Timeout Problem. In most cases - no connection */ +"Error.TimeOut" = "Das Zeitlimit für die Antwort wurde überschritten"; + /* Shared error: Request cancelled */ "Error.RequestCancelled" = "Request cancelled"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings index 0e9d10136..500b11011 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings @@ -569,6 +569,9 @@ /* Shared error: Network problems. In most cases - no connection */ "Error.NoNetwork" = "No connection"; +/* Shared error: Timeout Problem. In most cases - no connection */ +"Error.TimeOut" = "Response waiting time exceeded"; + /* Shared error: Request cancelled */ "Error.RequestCancelled" = "Request cancelled"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings index 56e672c06..f3721173d 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings @@ -570,6 +570,9 @@ /* Shared error: Network problems. In most cases - no connection */ "Error.NoNetwork" = "Нет соединения с сетью"; +/* Shared error: Timeout Problem. In most cases - no connection */ +"Error.TimeOut" = "Превышено время ожидания ответа"; + /* Shared error: Request cancelled */ "Error.RequestCancelled" = "Запрос отменён"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings index fbbd33ae4..6920e70fd 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings @@ -562,6 +562,9 @@ /* Shared error: Network problems. In most cases - no connection */ "Error.NoNetwork" = "无连接"; +/* Shared error: Timeout Problem. In most cases - no connection */ +"Error.TimeOut" = "超过响应等待时间"; + /* Shared error: Request cancelled */ "Error.RequestCancelled" = "请求已取消"; diff --git a/CommonKit/Sources/CommonKit/Helpers/Deadline.swift b/CommonKit/Sources/CommonKit/Helpers/Deadline.swift new file mode 100644 index 000000000..b543193e9 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/Deadline.swift @@ -0,0 +1,76 @@ +// +// Deadline.swift +// CommonKit +// +// Created by Christian Benua on 06.02.2025. +// + +import Foundation +import QuartzCore + +public func deadline( + until instant: TimeInterval, + isolation: isolated (any Actor)? = #isolation, + operation: @Sendable () async throws -> R +) async throws -> R where R: Sendable { + let result = await withoutActuallyEscaping(operation) { operation in + await withTaskGroup( + of: DeadlineState.self, + returning: Result.self, + isolation: isolation + ) { taskGroup in + + taskGroup.addTask { + do { + let result = try await operation() + return .operationResult(.success(result)) + } catch { + return .operationResult(.failure(error)) + } + } + + taskGroup.addTask { + do { + let interval = instant - CACurrentMediaTime() + guard interval > 0 else { + return .sleepResult(.failure(DeadlineExceededError())) + } + try await Task.sleep(interval: interval) + return .sleepResult(.success(false)) + } catch where Task.isCancelled { + return .sleepResult(.success(true)) + } catch { + return .sleepResult(.failure(error)) + } + } + + defer { + taskGroup.cancelAll() + } + + for await next in taskGroup { + switch next { + case .operationResult(let result): + return result + case .sleepResult(.success(false)): + return .failure(DeadlineExceededError()) + case .sleepResult(.success(true)): + continue + case .sleepResult(.failure(let error)): + return .failure(error) + } + } + + preconditionFailure("Invalid state") + } + } + + return try result.get() +} + +enum DeadlineState: Sendable where T: Sendable { + case operationResult(Result) + case sleepResult(Result) +} + +public struct DeadlineExceededError: Error {} diff --git a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift index bd4768254..9a1d5697b 100644 --- a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift +++ b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift @@ -60,6 +60,9 @@ public extension String.adamant { public static var networkError: String { String.localized("Error.NoNetwork", comment: "Shared error: Network problems. In most cases - no connection") } + public static var timeoutError: String { + String.localized("Error.TimeOut", comment: "Shared error: Timeout Problem. In most cases - no connection") + } public static var requestCancelled: String { String.localized("Error.RequestCancelled", comment: "Shared error: Request cancelled") } diff --git a/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift index e2ecb5b11..633f5911c 100644 --- a/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift +++ b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift @@ -85,7 +85,7 @@ extension ApiServiceError: Equatable { } } -extension ApiServiceError: HealthCheckableError { +extension ApiServiceError: HealthCheckableTimeoutableError { public var isNetworkError: Bool { switch self { case .networkError: @@ -99,6 +99,10 @@ extension ApiServiceError: HealthCheckableError { .networkError(error: AdamantError(message: .adamant.sharedErrors.networkError)) } + public static var timeoutError: ApiServiceError { + .networkError(error: AdamantError(message: .adamant.sharedErrors.timeoutError)) + } + public static func noEndpointsError(nodeGroupName: String) -> ApiServiceError { .noEndpointsAvailable(nodeGroupName: nodeGroupName) } diff --git a/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift index 6ad63f0c1..90d11252b 100644 --- a/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift @@ -93,11 +93,13 @@ public protocol AdamantApiServiceProtocol: ApiServiceProtocol { func sendTransaction( path: String, - transaction: UnregisteredTransaction + transaction: UnregisteredTransaction, + timeout: TimeInterval? ) async -> ApiServiceResult func sendMessageTransaction( - transaction: UnregisteredTransaction + transaction: UnregisteredTransaction, + timeout: TimeInterval? ) async -> ApiServiceResult // MARK: - Delegates diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift index cffb1982b..954bb26cd 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift @@ -55,11 +55,13 @@ extension AdamantApiService { } public func sendMessageTransaction( - transaction: UnregisteredTransaction + transaction: UnregisteredTransaction, + timeout: TimeInterval? = nil ) async -> ApiServiceResult { await sendTransaction( path: ApiCommands.Chats.processTransaction, - transaction: transaction + transaction: transaction, + timeout: timeout ) } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift index bf6052ac8..7c271d7a8 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift @@ -20,9 +20,10 @@ public extension ApiCommands { extension AdamantApiService { public func sendTransaction( path: String, - transaction: UnregisteredTransaction + transaction: UnregisteredTransaction, + timeout: TimeInterval? = nil ) async -> ApiServiceResult { - let response: ApiServiceResult = await request { core, origin in + let response: ApiServiceResult = await request(timeout: timeout) { core, origin in await core.sendRequestJsonResponse( origin: origin, path: path, diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift index 7a3a8b481..8a810aacd 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift @@ -22,16 +22,35 @@ public final class AdamantApiService { public func request( waitsForConnectivity: Bool = false, + timeout: TimeInterval? = nil, _ request: @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> ApiServiceResult { - await service.request( - waitsForConnectivity: waitsForConnectivity - ) { admApiCore, origin in - await request(admApiCore.apiCore, origin) + if let timeout { + await service.request( + waitsForConnectivity: waitsForConnectivity, + timeout: timeout + ) { admApiCore, origin in + await request(admApiCore.apiCore, origin) + } + } else { + await service.request( + waitsForConnectivity: waitsForConnectivity + ) { admApiCore, origin in + await request(admApiCore.apiCore, origin) + } } } } +extension AdamantApiServiceProtocol { + public func sendTransaction( + path: String, + transaction: UnregisteredTransaction + ) async -> ApiServiceResult { + await sendTransaction(path: path, transaction: transaction, timeout: nil) + } +} + extension AdamantApiService: AdamantApiServiceProtocol { @MainActor public var nodesInfoPublisher: AnyObservable { service.nodesInfoPublisher } diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift index fefbffad6..739539fe4 100644 --- a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift @@ -18,6 +18,10 @@ public protocol HealthCheckableError: Error { static func noEndpointsError(nodeGroupName: String) -> Self } +public protocol HealthCheckableTimeoutableError: HealthCheckableError { + static var timeoutError: Self { get } +} + @HealthCheckActor open class HealthCheckWrapper: Sendable { @ObservableValue private(set) var nodes: [Node] = .init() @@ -129,6 +133,29 @@ open class HealthCheckWrapper: S open func healthCheckInternal() async {} } +public extension HealthCheckWrapper where Error: HealthCheckableTimeoutableError { + func request( + waitsForConnectivity: Bool, + timeout: TimeInterval, + _ requestAction: @Sendable (Service, NodeOrigin) async -> Result + ) async -> Result { + let startTime = CACurrentMediaTime() + + do { + let result = try await deadline(until: startTime + timeout) { + await self.request(waitsForConnectivity: waitsForConnectivity, requestAction) + } + return result + } catch _ as DeadlineExceededError { + return .failure(.timeoutError) + } catch let error as Error { + return .failure(error) + } catch { + return .failure(.noNetworkError) + } + } +} + private extension HealthCheckWrapper { private enum AppState { case active From ef5b83e01c69e96a03cc7912431fe959e93b2967 Mon Sep 17 00:00:00 2001 From: ChrisBenua Date: Mon, 24 Feb 2025 22:50:49 +0300 Subject: [PATCH 2/7] [trello.com/c/0dgGzsPE]: fix deadline visibility modifier --- CommonKit/Sources/CommonKit/Helpers/Deadline.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CommonKit/Sources/CommonKit/Helpers/Deadline.swift b/CommonKit/Sources/CommonKit/Helpers/Deadline.swift index b543193e9..4debf3b47 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Deadline.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Deadline.swift @@ -8,7 +8,9 @@ import Foundation import QuartzCore -public func deadline( +/// Executes operation `operation` with deadline `instant` +/// Supports actor isolation +func deadline( until instant: TimeInterval, isolation: isolated (any Actor)? = #isolation, operation: @Sendable () async throws -> R From 5ad0fbcbc0001cb4e184daa198f004c156dc649f Mon Sep 17 00:00:00 2001 From: Ruslan Alikhamov Date: Sun, 13 Apr 2025 00:16:33 +0500 Subject: [PATCH 3/7] [trello.com/c/0dgGzsPE] removed warnings --- .../CommonKit/Services/HealthCheck/HealthCheckWrapper.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift index 5c96e11a2..22a3b58cc 100644 --- a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift @@ -140,7 +140,6 @@ open class HealthCheckWrapper: S open func healthCheckInternal() async {} } -//<<<<<<< HEAD extension HealthCheckWrapper where Error: HealthCheckableTimeoutableError { public func request( waitsForConnectivity: Bool, @@ -165,9 +164,6 @@ extension HealthCheckWrapper where Error: HealthCheckableTimeoutableError { } extension HealthCheckWrapper { - //======= - //extension HealthCheckWrapper { - //>>>>>>> develop private enum AppState { case active case background From 9a72686d026da718d6a0b7a9ce3ad3495049425c Mon Sep 17 00:00:00 2001 From: Ruslan Alikhamov Date: Sun, 13 Apr 2025 12:16:09 +0500 Subject: [PATCH 4/7] [trello.com/c/0dgGzsPE] added missing var --- .../AdmWalletService+DynamicConstants.swift | 131 ------------------ ...AdmWalletService+RichMessageProvider.swift | 4 + 2 files changed, 4 insertions(+), 131 deletions(-) delete mode 100644 Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift deleted file mode 100644 index 17182ea42..000000000 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension AdmWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 0.5 - static let currencySymbol = "ADM" - static let currencyExponent: Int = -8 - static let qqPrefix: String = "adm" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 300, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 10, - normalServiceUpdateInterval: 300, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var newPendingInterval: Int { - 4000 - } - - static var oldPendingInterval: Int { - 4000 - } - - static var registeredInterval: Int { - 4000 - } - - static var timeouts: MessageTimeouts { - MessageTimeouts(message: 300, attachment: 300) - } - - var tokenName: String { - "ADAMANT Messenger" - } - - var consistencyMaxTime: Double { - 0 - } - - var minBalance: Decimal { - 0 - } - - var minAmount: Decimal { - 0 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 0 - } - - static var minNodeVersion: String? { - "0.8.0" - } - - var transferDecimals: Int { - 8 - } - - static let explorerTx = "https://explorer.adamant.im/tx/" - static let explorerAddress = "https://explorer.adamant.im/address/" - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://clown.adamant.im")!), - Node.makeDefaultNode(url: URL(string: "https://lake.adamant.im")!), - Node.makeDefaultNode( - url: URL(string: "https://endless.adamant.im")!, - altUrl: URL(string: "http://149.102.157.15:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://bid.adamant.im")!), - Node.makeDefaultNode(url: URL(string: "https://unusual.adamant.im")!), - Node.makeDefaultNode( - url: URL(string: "https://debate.adamant.im")!, - altUrl: URL(string: "http://95.216.161.113:36666") - ), - Node.makeDefaultNode(url: URL(string: "http://78.47.205.206:36666")!), - Node.makeDefaultNode(url: URL(string: "http://5.161.53.74:36666")!), - Node.makeDefaultNode(url: URL(string: "http://184.94.215.92:45555")!), - Node.makeDefaultNode( - url: URL(string: "https://node1.adamant.business")!, - altUrl: URL(string: "http://194.233.75.29:45555") - ), - Node.makeDefaultNode(url: URL(string: "https://node2.blockchain2fa.io")!), - Node.makeDefaultNode( - url: URL(string: "https://phecda.adm.im")!, - altUrl: URL(string: "http://46.250.234.248:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://tegmine.adm.im")!), - Node.makeDefaultNode( - url: URL(string: "https://tauri.adm.im")!, - altUrl: URL(string: "http://154.26.159.245:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://dschubba.adm.im")!), - Node.makeDefaultNode( - url: URL(string: "https://tauri.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:36666") - ), - Node.makeDefaultNode( - url: URL(string: "https://endless.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:46666") - ), - Node.makeDefaultNode( - url: URL(string: "https://debate.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:56666") - ) - ] - } - - static var serviceNodes: [Node] { - [ - Node.makeDefaultNode( - url: URL(string: "https://info.adamant.im")!, - altUrl: URL(string: "http://5.161.98.136:33088")! - ), - Node.makeDefaultNode( - url: URL(string: "https://info2.adm.im")!, - altUrl: URL(string: "http://207.180.210.95:33088")! - ) - ] - } -} diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift index 55f466983..81298f03d 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift @@ -36,6 +36,10 @@ extension AdmWalletService { return type(of: self).richMessageType } + static var timeouts: MessageTimeouts { + MessageTimeouts(message: 300, attachment: 300) + } + // MARK: Events func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { From 2dbbf85dcad0b365b7048006b3076df49bd649b1 Mon Sep 17 00:00:00 2001 From: Ruslan Alikhamov Date: Mon, 14 Apr 2025 02:24:52 +0500 Subject: [PATCH 5/7] [trello.com/c/0dgGzsPE-4] Refactored error handling --- .../ApiService/AdamantApiService.swift | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift index 379fe0124..25eb13cb0 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift @@ -18,26 +18,23 @@ import Foundation /// We are creating a `AdamantApiTask` without any `Task` in it to defer a `Task` execution to store it in the storage before the execution /// -public final class AdamantApiService: @unchecked Sendable { - @Atomic private var adamantApiTaskStorage: [UUID: CancellableTask] = [:] - private var cancelled: Bool = false private final actor TasksStorage { private var adamantApiTaskStorage: [UUID: CancellableTask] = [:] - + init() {} - + func getTask(id: UUID) -> CancellableTask? { return adamantApiTaskStorage[id] } - + func addTask(_ task: CancellableTask, id: UUID) { adamantApiTaskStorage[id] = task } - + func removeTask(id: UUID) { adamantApiTaskStorage[id] = nil } - + func cancelAll() { adamantApiTaskStorage.forEach { _, task in task.cancel() @@ -65,21 +62,36 @@ public final class AdamantApiService: @unchecked Sendable { ) async -> ApiServiceResult { let taskId: UUID = .init() let task = AdamantApiTask(id: taskId) - + await tasksStorage.addTask(task, id: taskId) defer { Task { await tasksStorage.removeTask(id: taskId) } } - + task.startTask( Task { - await service.request( - waitsForConnectivity: waitsForConnectivity, - taskId: taskId, - isCancelled: { - return await tasksStorage.getTask(id: taskId)?.isCancelled ?? true + if let timeout { + await service.request( + waitsForConnectivity: waitsForConnectivity, + timeout: timeout, + taskId: taskId, + isCancelled: { + await tasksStorage.getTask(id: taskId)?.isCancelled ?? true + } + ) { admApiCore, origin in + await request(admApiCore.apiCore, origin) + } + } else { + await service.request( + waitsForConnectivity: waitsForConnectivity, + taskId: taskId, + isCancelled: { + await tasksStorage.getTask(id: taskId)?.isCancelled ?? true + } + ) { admApiCore, origin in + await request(admApiCore.apiCore, origin) } } } @@ -120,7 +132,7 @@ public final class AdamantHealthCheck: BlockchainHealthCheckWrapper Result ) async -> Result { var usedNodesIds: Set = .init() - var lastConnectionError: Error? + var lastConnectionError: AdamantApiCore.Error? /// Check the cancellation of the task from the outside guard await !isCancelled() else { @@ -146,14 +158,24 @@ public final class AdamantHealthCheck: BlockchainHealthCheckWrapper Date: Mon, 14 Apr 2025 03:12:58 +0500 Subject: [PATCH 6/7] [trello.com/c/0dgGzsPE-4] Fixed build error --- .../Services/ApiService/AdamantApi+AdamantApiTask.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift index 3e012c8f2..295d8d65f 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift @@ -10,6 +10,7 @@ import Foundation final class AdamantApiTask: CancellableTask { private var task: Task, Never>! private let id: UUID + private var cancelled: Bool = false var isCancelled: Bool { cancelled From 0d28ad29bfb0668bf2a2252b98f12795b504baaa Mon Sep 17 00:00:00 2001 From: Ruslan Alikhamov Date: Tue, 15 Apr 2025 01:26:39 +0500 Subject: [PATCH 7/7] [trello.com/c/0dgGzsPE-4] Temporary change regarding timeout-able calls --- .../ApiService/AdamantApiService.swift | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift index 25eb13cb0..3e19f81dd 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift @@ -7,6 +7,7 @@ // import Foundation +import QuartzCore.CABase /// /// I need to override HealthCheckWrapped because of now we have an option to cancel the tasks @@ -159,12 +160,29 @@ public final class AdamantHealthCheck: BlockchainHealthCheckWrapper