import Foundation /// Synor DEX SDK Client /// /// Complete decentralized exchange client with support for: /// - AMM swaps (constant product, stable, concentrated) /// - Liquidity provision /// - Perpetual futures (up to 100x leverage) /// - Order books (limit orders) /// - Farming & staking public class SynorDex { private let config: DexConfig private let session: URLSession private var closed = false public let perps: PerpsClient public let orderbook: OrderBookClient public let farms: FarmsClient public init(config: DexConfig) { self.config = config let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = TimeInterval(config.timeout) / 1000 self.session = URLSession(configuration: configuration) self.perps = PerpsClient() self.orderbook = OrderBookClient() self.farms = FarmsClient() self.perps.dex = self self.orderbook.dex = self self.farms.dex = self } // MARK: - Token Operations public func getToken(address: String) async throws -> Token { try await get("/tokens/\(address)") } public func listTokens() async throws -> [Token] { try await get("/tokens") } public func searchTokens(query: String) async throws -> [Token] { let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query return try await get("/tokens/search?q=\(encoded)") } // MARK: - Pool Operations public func getPool(tokenA: String, tokenB: String) async throws -> Pool { try await get("/pools/\(tokenA)/\(tokenB)") } public func getPoolById(poolId: String) async throws -> Pool { try await get("/pools/\(poolId)") } public func listPools(filter: PoolFilter? = nil) async throws -> [Pool] { var params: [String] = [] if let f = filter { if let tokens = f.tokens { params.append("tokens=\(tokens.joined(separator: ","))") } if let minTvl = f.minTvl { params.append("min_tvl=\(minTvl)") } if let minVolume = f.minVolume24h { params.append("min_volume=\(minVolume)") } if let verified = f.verified { params.append("verified=\(verified)") } if let limit = f.limit { params.append("limit=\(limit)") } if let offset = f.offset { params.append("offset=\(offset)") } } let path = params.isEmpty ? "/pools" : "/pools?\(params.joined(separator: "&"))" return try await get(path) } // MARK: - Swap Operations public func getQuote(params: QuoteParams) async throws -> Quote { try await post("/swap/quote", body: [ "token_in": params.tokenIn, "token_out": params.tokenOut, "amount_in": String(params.amountIn), "slippage": params.slippage ?? 0.005 ]) } public func swap(params: SwapParams) async throws -> SwapResult { let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200 var body: [String: Any] = [ "token_in": params.tokenIn, "token_out": params.tokenOut, "amount_in": String(params.amountIn), "min_amount_out": String(params.minAmountOut), "deadline": deadline ] if let recipient = params.recipient { body["recipient"] = recipient } return try await post("/swap", body: body) } // MARK: - Liquidity Operations public func addLiquidity(params: AddLiquidityParams) async throws -> LiquidityResult { let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200 var body: [String: Any] = [ "token_a": params.tokenA, "token_b": params.tokenB, "amount_a": String(params.amountA), "amount_b": String(params.amountB), "deadline": deadline ] if let minA = params.minAmountA { body["min_amount_a"] = String(minA) } if let minB = params.minAmountB { body["min_amount_b"] = String(minB) } return try await post("/liquidity/add", body: body) } public func removeLiquidity(params: RemoveLiquidityParams) async throws -> LiquidityResult { let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200 var body: [String: Any] = [ "pool": params.pool, "lp_amount": String(params.lpAmount), "deadline": deadline ] if let minA = params.minAmountA { body["min_amount_a"] = String(minA) } if let minB = params.minAmountB { body["min_amount_b"] = String(minB) } return try await post("/liquidity/remove", body: body) } public func getMyPositions() async throws -> [LPPosition] { try await get("/liquidity/positions") } // MARK: - Analytics public func getPriceHistory(pair: String, interval: String, limit: Int = 100) async throws -> [OHLCV] { try await get("/analytics/candles/\(pair)?interval=\(interval)&limit=\(limit)") } public func getTradeHistory(pair: String, limit: Int = 50) async throws -> [TradeHistory] { try await get("/analytics/trades/\(pair)?limit=\(limit)") } public func getVolumeStats() async throws -> VolumeStats { try await get("/analytics/volume") } public func getTVL() async throws -> TVLStats { try await get("/analytics/tvl") } // MARK: - Lifecycle public func healthCheck() async -> Bool { do { let response: HealthResponse = try await get("/health") return response.status == "healthy" } catch { return false } } public func close() { closed = true session.invalidateAndCancel() } public var isClosed: Bool { closed } // MARK: - Internal Methods func get(_ path: String) async throws -> T { try checkClosed() var request = URLRequest(url: URL(string: config.endpoint + path)!) request.httpMethod = "GET" addHeaders(&request) return try await execute(request) } func post(_ path: String, body: [String: Any]) async throws -> T { try checkClosed() var request = URLRequest(url: URL(string: config.endpoint + path)!) request.httpMethod = "POST" request.httpBody = try JSONSerialization.data(withJSONObject: body) addHeaders(&request) return try await execute(request) } func delete(_ path: String) async throws -> T { try checkClosed() var request = URLRequest(url: URL(string: config.endpoint + path)!) request.httpMethod = "DELETE" addHeaders(&request) return try await execute(request) } private func addHeaders(_ request: inout URLRequest) { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version") } private func execute(_ request: URLRequest) async throws -> T { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw DexError.network("Invalid response") } guard (200...299).contains(httpResponse.statusCode) else { let error = try? JSONDecoder().decode(ErrorResponse.self, from: data) throw DexError.http( message: error?.message ?? "HTTP \(httpResponse.statusCode)", code: error?.code, status: httpResponse.statusCode ) } return try JSONDecoder().decode(T.self, from: data) } private func checkClosed() throws { if closed { throw DexError.clientClosed } } private struct HealthResponse: Decodable { let status: String } private struct ErrorResponse: Decodable { let message: String? let code: String? } } /// DEX client configuration public struct DexConfig { public let apiKey: String public let endpoint: String public let wsEndpoint: String public let timeout: Int public let retries: Int public let debug: Bool public init( apiKey: String, endpoint: String = "https://dex.synor.io/v1", wsEndpoint: String = "wss://dex.synor.io/v1/ws", timeout: Int = 30000, retries: Int = 3, debug: Bool = false ) { self.apiKey = apiKey self.endpoint = endpoint self.wsEndpoint = wsEndpoint self.timeout = timeout self.retries = retries self.debug = debug } } /// DEX SDK Error public enum DexError: Error { case http(message: String, code: String?, status: Int) case network(String) case clientClosed } /// Perpetual futures sub-client public class PerpsClient { weak var dex: SynorDex? public func listMarkets() async throws -> [PerpMarket] { try await dex!.get("/perps/markets") } public func getMarket(symbol: String) async throws -> PerpMarket { try await dex!.get("/perps/markets/\(symbol)") } public func openPosition(params: OpenPositionParams) async throws -> PerpPosition { var body: [String: Any] = [ "market": params.market, "side": params.side.rawValue, "size": String(params.size), "leverage": params.leverage, "order_type": params.orderType.rawValue, "margin_type": params.marginType.rawValue, "reduce_only": params.reduceOnly ] if let price = params.limitPrice { body["limit_price"] = price } if let sl = params.stopLoss { body["stop_loss"] = sl } if let tp = params.takeProfit { body["take_profit"] = tp } return try await dex!.post("/perps/positions", body: body) } public func closePosition(params: ClosePositionParams) async throws -> PerpPosition { var body: [String: Any] = [ "market": params.market, "order_type": params.orderType.rawValue ] if let size = params.size { body["size"] = String(size) } if let price = params.limitPrice { body["limit_price"] = price } return try await dex!.post("/perps/positions/close", body: body) } public func getPositions() async throws -> [PerpPosition] { try await dex!.get("/perps/positions") } public func getOrders() async throws -> [PerpOrder] { try await dex!.get("/perps/orders") } public func getFundingHistory(market: String, limit: Int = 100) async throws -> [FundingPayment] { try await dex!.get("/perps/funding/\(market)?limit=\(limit)") } } /// Order book sub-client public class OrderBookClient { weak var dex: SynorDex? public func getOrderBook(market: String, depth: Int = 20) async throws -> OrderBook { try await dex!.get("/orderbook/\(market)?depth=\(depth)") } public func placeLimitOrder(params: LimitOrderParams) async throws -> Order { try await dex!.post("/orderbook/orders", body: [ "market": params.market, "side": params.side, "price": params.price, "size": String(params.size), "time_in_force": params.timeInForce.rawValue, "post_only": params.postOnly ]) } public func getOpenOrders(market: String? = nil) async throws -> [Order] { let path = market.map { "/orderbook/orders?market=\($0)" } ?? "/orderbook/orders" return try await dex!.get(path) } } /// Farms sub-client public class FarmsClient { weak var dex: SynorDex? public func listFarms() async throws -> [Farm] { try await dex!.get("/farms") } public func getFarm(farmId: String) async throws -> Farm { try await dex!.get("/farms/\(farmId)") } public func stake(params: StakeParams) async throws -> FarmPosition { try await dex!.post("/farms/stake", body: [ "farm": params.farm, "amount": String(params.amount) ]) } public func getMyFarmPositions() async throws -> [FarmPosition] { try await dex!.get("/farms/positions") } }