import Foundation /// Minimal XRP Ledger JSON-RPC client. A couple of public endpoints are tried /// in order so a single outage does not break the wallet. final class XRPClient { static let shared = XRPClient() private let endpoints = [ "https://s1.ripple.com:51234/", "https://s2.ripple.com:51234/", "https://xrplcluster.com/", ] private var preferredEndpoint = 0 private let session: URLSession private init() { let configuration = URLSessionConfiguration.ephemeral configuration.timeoutIntervalForRequest = 20 configuration.waitsForConnectivity = false session = URLSession(configuration: configuration) } func rpc(_ method: String, _ params: [String: Any] = [:]) async throws -> [String: Any] { var lastError: Error = XRPError.network("XRP network is unavailable") for attempt in 0.. [String: Any] { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: Any] = ["method": method, "params": [params], "id": 1] request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw XRPError.network("Ledger request failed (\(method))") } guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw XRPError.network("Malformed ledger response") } let result = json["result"] as? [String: Any] ?? [:] if let error = result["error"] as? String { let message = result["error_message"] as? String ?? error throw XRPError.rpc(message) } if let error = json["error"] as? String { throw XRPError.rpc(json["error_message"] as? String ?? error) } return result } static func isAccountNotFound(_ error: Error) -> Bool { if case XRPError.rpc(let message) = error { return message == "actNotFound" || message.contains("Account not found") } return false } }