// // OnBoardingService.swift // Malinka // // Created by Juraldinio on 12/15/22. // Copyright © 2022 NUT.Tech. All rights reserved. // import Foundation import UIKit import EosioSwift import Combine import WalletNetwork struct OnBoardingService { enum ServiceError: Error { case nodes(cause: String) case hyperions(cause: String) case version(cause: String) } let environment: ApplicationEnvironment func loadVersion() async throws { struct Version { static let invalid = 1000 let node: String let timeSpent: TimeInterval let major: Int let minor: Int let patch: Int let build: Int var isValid: Bool { self.major != Self.invalid } static func createInvalid(for node: String) -> Version { Version(node: node, timeSpent: 0, major: Self.invalid, minor: Self.invalid, patch: Self.invalid, build: Self.invalid) } } let systemName = await UIDevice.current.systemName let nodes = self.environment.current.nodes let results = await withTaskGroup(of: Version.self) { taskGroup -> [Version] in nodes.forEach { node in taskGroup.addTask { let startTime = Date().timeIntervalSince1970 let result = await Network.Service.Blockchain .queryRaw(endpoint: node, contract: .appSettings, scope: self.environment.current.contract(.appSettings), table: "build") let timeSpent = Date().timeIntervalSince1970 - startTime // print("Node \(node) loaded version in \(timeSpent) seconds") if let rows = result.value, let minBuildVersionString = rows.compactMap({ $0["min_version_build"] as? String }).first, let minBuildVersionJson = (try? JSONSerialization.jsonObject(with: minBuildVersionString.data(using: .utf8) ?? Data(), options: [])) as? [String: Any], let versions = (minBuildVersionJson[systemName] as? String)?.split(separator: "."), let major = Int(versions.first ?? ""), let minor = Int(versions.dropFirst().first ?? ""), let patch = Int(versions.dropFirst().dropFirst().first ?? ""), let build = Int(versions.dropFirst().dropFirst().dropFirst().first ?? "0") { return Version(node: node, timeSpent: timeSpent, major: major, minor: minor, patch: patch, build: build) } return Version.createInvalid(for: node) } } var childTaskResults = [Version]() for await result in taskGroup { if !childTaskResults.isEmpty { print("Too late for version from \(result.node) some node was faster \(childTaskResults.sorted(by: { $0.timeSpent < $1.timeSpent }).map { $0.node }.joined(separator: ", "))") } childTaskResults.append(result) if result.major < 1000 { self.environment.current .update(nodes: [result.node] + nodes.filter { $0 != result.node }) return [result] } } return childTaskResults } // let node = results.sorted(by: { $0.1 < $1.1 }) if let result = results.first(where: { $0.isValid }) { if Bundle.main.major < result.major { throw ServiceError.version(cause: L10n.Main.wrongVersion) } else if Bundle.main.major == result.major { if Bundle.main.minor < result.minor { throw ServiceError.version(cause: L10n.Main.wrongVersion) } else if Bundle.main.minor == result.minor { if Bundle.main.patch < result.patch { throw ServiceError.version(cause: L10n.Main.wrongVersion) } else if Bundle.main.patch == result.patch { if Bundle.main.build < result.build { throw ServiceError.version(cause: L10n.Main.wrongVersion) } } } } } else { throw ServiceError.version(cause: L10n.Main.noVersion) } } func loadNodes() async throws { let record = self.environment.current let environment = try record.networkEnvironment() let service = EOSService(environment: environment) let nodes = await service.requireNodes() if nodes.isEmpty { throw ServiceError.nodes(cause: L10n.Main.noNodes) } self.environment.current.update(nodes: nodes) } func loadHyperions() async throws { let record = self.environment.current let environment = try record.networkEnvironment() let service = EOSService(environment: environment) let hyperions = await service.requireHyperions() let finalHyperions = hyperions.isEmpty ? Self.defaultHyperions(for: record.backend.name) : hyperions if finalHyperions.isEmpty { throw ServiceError.hyperions(cause: L10n.Main.noHyperion) } self.environment.current.update(hyperions: finalHyperions) } @discardableResult func loadAppSettings(nodes: [String]) async -> AppSettingsModel { guard let node = nodes.first else { return AppSettingsModel.shared() } let result = await Network.Service.Blockchain .queryRaw(endpoint: node, contract: .appSettings, scope: ApplicationEnvironment.shared().current.contract(.appSettings), table: "config") guard let rows = result.value else { return AppSettingsModel.shared() } let iosData = rows.compactMap { row -> [String: Any]? in if row["device"] as? String == "ios" { return row } return nil }.first if let value = iosData?["data"] as? String, let data = value.data(using: .utf8) { return AppSettingsModel.shared(with: data) } return AppSettingsModel.shared() } // MARK: - Private private static func defaultHyperions(for backend: String) -> [String] { guard let url = Bundle.main.url(forResource: "NetworkSettings", withExtension: "bundle"), let networkBundle = Bundle(url: url), let hyperionsDict = networkBundle.infoDictionary?["hyperions"] as? [String: [String]], let hyperions = hyperionsDict[backend] else { return [] } return hyperions } }