import Foundation /// Synor RPC SDK client for Swift. /// /// Provides blockchain queries, transaction submission, and real-time /// subscriptions via WebSocket. /// /// Example: /// ```swift /// let rpc = SynorRpc(config: RpcConfig(apiKey: "your-api-key")) /// /// // Get latest block /// let block = try await rpc.getLatestBlock() /// print("Latest block: \(block.height)") /// /// // Subscribe to new blocks /// let subscription = try await rpc.subscribeBlocks { block in /// print("New block: \(block.height)") /// } /// /// // Later: cancel subscription /// subscription.cancel() /// ``` public class SynorRpc { private let config: RpcConfig private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder private var wsTask: URLSessionWebSocketTask? private var subscriptions: [String: (String) -> Void] = [:] private let subscriptionLock = NSLock() public init(config: RpcConfig) { 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() } deinit { wsTask?.cancel(with: .goingAway, reason: nil) } // MARK: - Block Operations /// Get the latest block. public func getLatestBlock() async throws -> Block { return try await get(path: "/blocks/latest") } /// Get a block by hash or height. public func getBlock(hashOrHeight: String) async throws -> Block { return try await get(path: "/blocks/\(hashOrHeight)") } /// Get a block header by hash or height. public func getBlockHeader(hashOrHeight: String) async throws -> BlockHeader { return try await get(path: "/blocks/\(hashOrHeight)/header") } /// Get blocks in a range. public func getBlocks(startHeight: Int64, endHeight: Int64) async throws -> [Block] { return try await get(path: "/blocks?start=\(startHeight)&end=\(endHeight)") } // MARK: - Transaction Operations /// Get a transaction by ID. public func getTransaction(txid: String) async throws -> RpcTransaction { return try await get(path: "/transactions/\(txid)") } /// Get raw transaction hex. public func getRawTransaction(txid: String) async throws -> String { let response: RawTxResponse = try await get(path: "/transactions/\(txid)/raw") return response.hex } /// Send a raw transaction. public func sendRawTransaction(hex: String) async throws -> SubmitResult { let request = SendRawTxRequest(hex: hex) return try await post(path: "/transactions/send", body: request) } /// Decode a raw transaction without broadcasting. public func decodeRawTransaction(hex: String) async throws -> RpcTransaction { let request = DecodeRawTxRequest(hex: hex) return try await post(path: "/transactions/decode", body: request) } /// Get transactions for an address. public func getAddressTransactions( address: String, limit: Int = 50, offset: Int = 0 ) async throws -> [RpcTransaction] { return try await get(path: "/addresses/\(address)/transactions?limit=\(limit)&offset=\(offset)") } // MARK: - Fee Estimation /// Estimate fee for a given priority. public func estimateFee(priority: RpcPriority = .medium) async throws -> FeeEstimate { return try await get(path: "/fees/estimate?priority=\(priority.rawValue)") } /// Get all fee estimates. public func getAllFeeEstimates() async throws -> [RpcPriority: FeeEstimate] { let estimates: [FeeEstimate] = try await get(path: "/fees/estimates") return Dictionary(uniqueKeysWithValues: estimates.map { ($0.priority, $0) }) } // MARK: - Chain Information /// Get chain information. public func getChainInfo() async throws -> ChainInfo { return try await get(path: "/chain/info") } /// Get mempool information. public func getMempoolInfo() async throws -> MempoolInfo { return try await get(path: "/mempool/info") } /// Get mempool transactions. public func getMempoolTransactions(limit: Int = 100) async throws -> [String] { return try await get(path: "/mempool/transactions?limit=\(limit)") } // MARK: - WebSocket Subscriptions /// Subscribe to new blocks. public func subscribeBlocks(callback: @escaping (Block) -> Void) async throws -> Subscription { return try await subscribe(channel: "blocks", filter: nil) { [weak self] data in guard let self = self else { return } if let block = try? self.decoder.decode(Block.self, from: Data(data.utf8)) { callback(block) } } } /// Subscribe to transactions for a specific address. public func subscribeAddress( address: String, callback: @escaping (RpcTransaction) -> Void ) async throws -> Subscription { return try await subscribe(channel: "address", filter: address) { [weak self] data in guard let self = self else { return } if let tx = try? self.decoder.decode(RpcTransaction.self, from: Data(data.utf8)) { callback(tx) } } } /// Subscribe to mempool transactions. public func subscribeMempool(callback: @escaping (RpcTransaction) -> Void) async throws -> Subscription { return try await subscribe(channel: "mempool", filter: nil) { [weak self] data in guard let self = self else { return } if let tx = try? self.decoder.decode(RpcTransaction.self, from: Data(data.utf8)) { callback(tx) } } } private func subscribe( channel: String, filter: String?, callback: @escaping (String) -> Void ) async throws -> Subscription { try await ensureWebSocketConnection() let subscriptionId = UUID().uuidString subscriptionLock.lock() subscriptions[subscriptionId] = callback subscriptionLock.unlock() let message = WsMessage(type: "subscribe", channel: channel, data: nil, filter: filter) let messageData = try encoder.encode(message) let messageString = String(data: messageData, encoding: .utf8)! try await wsTask?.send(.string(messageString)) return Subscription(id: subscriptionId, channel: channel) { [weak self] in self?.subscriptionLock.lock() self?.subscriptions.removeValue(forKey: subscriptionId) self?.subscriptionLock.unlock() Task { let unsubMessage = WsMessage(type: "unsubscribe", channel: channel, data: nil, filter: nil) if let data = try? self?.encoder.encode(unsubMessage), let str = String(data: data, encoding: .utf8) { try? await self?.wsTask?.send(.string(str)) } } } } private func ensureWebSocketConnection() async throws { guard wsTask == nil || wsTask?.state != .running else { return } guard let url = URL(string: "\(config.wsEndpoint)?token=\(config.apiKey)") else { throw RpcError.invalidResponse } wsTask = session.webSocketTask(with: url) wsTask?.resume() // Start receiving messages receiveMessages() } private func receiveMessages() { wsTask?.receive { [weak self] result in guard let self = self else { return } switch result { case .success(let message): switch message { case .string(let text): self.handleWebSocketMessage(text) case .data(let data): if let text = String(data: data, encoding: .utf8) { self.handleWebSocketMessage(text) } @unknown default: break } // Continue receiving self.receiveMessages() case .failure(let error): if self.config.debug { print("WebSocket error: \(error)") } } } } private func handleWebSocketMessage(_ text: String) { guard let data = text.data(using: .utf8), let message = try? decoder.decode(WsMessage.self, from: data), message.type == "data", let messageData = message.data else { return } subscriptionLock.lock() let callbacks = Array(subscriptions.values) subscriptionLock.unlock() for callback in callbacks { callback(messageData) } } // MARK: - HTTP Methods private func get(path: String) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "GET", path: path, body: nil as Empty?) } } private func post(path: String, body: R) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "POST", path: path, body: body) } } private func performRequest( method: String, path: String, body: R? ) async throws -> T { guard let url = URL(string: "\(config.endpoint)\(path)") else { throw RpcError.invalidResponse } var request = URLRequest(url: url) request.httpMethod = method request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") if let body = body { do { request.httpBody = try encoder.encode(body) } catch { throw RpcError.encodingError(error) } } let (data, response): (Data, URLResponse) do { (data, response) = try await session.data(for: request) } catch { throw RpcError.networkError(error) } guard let httpResponse = response as? HTTPURLResponse else { throw RpcError.invalidResponse } if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { do { return try decoder.decode(T.self, from: data) } catch { throw RpcError.decodingError(error) } } let message = String(data: data, encoding: .utf8) ?? "Unknown error" throw RpcError.httpError(statusCode: httpResponse.statusCode, message: message) } private func executeWithRetry(_ operation: () async throws -> T) async throws -> T { var lastError: Error? for attempt in 0..