import Foundation /// Synor Mining SDK client for Swift. /// /// Provides pool connections, block templates, hashrate stats, and GPU management. public class SynorMining { private let config: MiningConfig private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder private var closed = false private var activeConnection: StratumConnection? public init(config: MiningConfig) { self.config = config let sessionConfig = URLSessionConfiguration.default sessionConfig.timeoutIntervalForRequest = config.timeout sessionConfig.timeoutIntervalForResource = config.timeout * 2 self.session = URLSession(configuration: sessionConfig) self.decoder = JSONDecoder() self.encoder = JSONEncoder() } // MARK: - Pool Operations /// Connect to a mining pool. public func connect(pool: PoolConfig) async throws -> StratumConnection { var body: [String: Any] = ["url": pool.url, "user": pool.user] if let password = pool.password { body["password"] = password } if let algorithm = pool.algorithm { body["algorithm"] = algorithm } if let difficulty = pool.difficulty { body["difficulty"] = difficulty } let conn: StratumConnection = try await post(path: "/pool/connect", body: body) activeConnection = conn return conn } /// Disconnect from the pool. public func disconnect() async throws { guard activeConnection != nil else { return } let _: [String: Any] = try await post(path: "/pool/disconnect", body: [:] as [String: String]) activeConnection = nil } /// Get current connection. public func getConnection() async throws -> StratumConnection { let conn: StratumConnection = try await get(path: "/pool/connection") activeConnection = conn return conn } /// Get pool stats. public func getPoolStats() async throws -> PoolStats { return try await get(path: "/pool/stats") } /// Check if connected. public var isConnected: Bool { activeConnection?.status == .connected } // MARK: - Mining Operations /// Get block template. public func getBlockTemplate() async throws -> BlockTemplate { return try await get(path: "/mining/template") } /// Submit mined work. public func submitWork(_ work: MinedWork) async throws -> SubmitResult { let body: [String: Any] = [ "templateId": work.templateId, "nonce": work.nonce, "extraNonce": work.extraNonce, "timestamp": work.timestamp, "hash": work.hash ] return try await post(path: "/mining/submit", body: body) } /// Start mining. public func startMining(algorithm: String? = nil) async throws { var body: [String: Any] = [:] if let algorithm = algorithm { body["algorithm"] = algorithm } let _: [String: Any] = try await post(path: "/mining/start", body: body) } /// Stop mining. public func stopMining() async throws { let _: [String: Any] = try await post(path: "/mining/stop", body: [:] as [String: String]) } /// Check if mining. public func isMining() async throws -> Bool { let response: [String: Bool] = try await get(path: "/mining/status") return response["mining"] ?? false } // MARK: - Stats Operations /// Get hashrate. public func getHashrate() async throws -> Hashrate { return try await get(path: "/stats/hashrate") } /// Get mining stats. public func getStats() async throws -> MiningStats { return try await get(path: "/stats") } /// Get earnings. public func getEarnings(period: TimePeriod? = nil) async throws -> Earnings { let path = period != nil ? "/stats/earnings?period=\(period!.rawValue)" : "/stats/earnings" return try await get(path: path) } /// Get share stats. public func getShareStats() async throws -> ShareStats { return try await get(path: "/stats/shares") } // MARK: - Device Operations /// List devices. public func listDevices() async throws -> [MiningDevice] { let response: DevicesResponse = try await get(path: "/devices") return response.devices ?? [] } /// Get device. public func getDevice(deviceId: String) async throws -> MiningDevice { return try await get(path: "/devices/\(deviceId.urlEncoded)") } /// Set device config. public func setDeviceConfig(deviceId: String, config: DeviceConfig) async throws -> MiningDevice { var body: [String: Any] = ["enabled": config.enabled] if let intensity = config.intensity { body["intensity"] = intensity } if let powerLimit = config.powerLimit { body["powerLimit"] = powerLimit } if let coreClockOffset = config.coreClockOffset { body["coreClockOffset"] = coreClockOffset } if let memoryClockOffset = config.memoryClockOffset { body["memoryClockOffset"] = memoryClockOffset } if let fanSpeed = config.fanSpeed { body["fanSpeed"] = fanSpeed } return try await put(path: "/devices/\(deviceId.urlEncoded)/config", body: body) } /// Enable device. public func enableDevice(deviceId: String) async throws { _ = try await setDeviceConfig(deviceId: deviceId, config: DeviceConfig(enabled: true)) } /// Disable device. public func disableDevice(deviceId: String) async throws { _ = try await setDeviceConfig(deviceId: deviceId, config: DeviceConfig(enabled: false)) } // MARK: - Worker Operations /// List workers. public func listWorkers() async throws -> [WorkerInfo] { let response: WorkersResponse = try await get(path: "/workers") return response.workers ?? [] } /// Get worker. public func getWorker(workerId: String) async throws -> WorkerInfo { return try await get(path: "/workers/\(workerId.urlEncoded)") } /// Remove worker. public func removeWorker(workerId: String) async throws { let _: [String: Any] = try await delete(path: "/workers/\(workerId.urlEncoded)") } // MARK: - Algorithm Operations /// List algorithms. public func listAlgorithms() async throws -> [MiningAlgorithm] { let response: AlgorithmsResponse = try await get(path: "/algorithms") return response.algorithms ?? [] } /// Get algorithm. public func getAlgorithm(name: String) async throws -> MiningAlgorithm { return try await get(path: "/algorithms/\(name.urlEncoded)") } /// Switch algorithm. public func switchAlgorithm(_ algorithm: String) async throws { let _: [String: Any] = try await post(path: "/algorithms/switch", body: ["algorithm": algorithm]) } /// Get most profitable algorithm. public func getMostProfitable() async throws -> MiningAlgorithm { return try await get(path: "/algorithms/profitable") } // MARK: - Lifecycle public func close() { closed = true activeConnection = nil } public var isClosed: Bool { closed } public func healthCheck() async -> Bool { do { let response: [String: String] = try await get(path: "/health") return response["status"] == "healthy" } catch { return false } } // MARK: - Private Methods private func get(path: String) async throws -> T { return try await request(method: "GET", path: path) } private func post(path: String, body: Any) async throws -> T { return try await request(method: "POST", path: path, body: body) } private func put(path: String, body: Any) async throws -> T { return try await request(method: "PUT", path: path, body: body) } private func delete(path: String) async throws -> T { return try await request(method: "DELETE", path: path) } private func request(method: String, path: String, body: Any? = nil) async throws -> T { guard !closed else { throw MiningError(message: "Client has been closed") } var lastError: Error? for attempt in 0..(method: String, path: String, body: Any?) async throws -> T { guard let url = URL(string: config.endpoint + path) else { throw MiningError(message: "Invalid URL") } var request = URLRequest(url: url) request.httpMethod = method request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version") if let body = body { request.httpBody = try JSONSerialization.data(withJSONObject: body) } let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiningError(message: "Invalid response") } if httpResponse.statusCode >= 400 { let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any] throw MiningError( message: errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)", code: errorInfo?["code"] as? String, statusCode: httpResponse.statusCode ) } return try decoder.decode(T.self, from: data) } } extension String { fileprivate var urlEncoded: String { addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self } }